cct-cli 0.7.9.0__py3-none-any.whl

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 (325) hide show
  1. calc_terminal/__init__.py +14 -0
  2. calc_terminal/__main__.py +14 -0
  3. calc_terminal/activity.py +1334 -0
  4. calc_terminal/agent.py +3387 -0
  5. calc_terminal/agent_runtime.py +519 -0
  6. calc_terminal/ai_context.py +447 -0
  7. calc_terminal/ai_modes.py +752 -0
  8. calc_terminal/ai_personalization.py +286 -0
  9. calc_terminal/ai_preview_feedback.py +213 -0
  10. calc_terminal/aicore.py +2572 -0
  11. calc_terminal/anim.py +367 -0
  12. calc_terminal/app.py +3685 -0
  13. calc_terminal/art.py +639 -0
  14. calc_terminal/atomsim.py +368 -0
  15. calc_terminal/attachments.py +743 -0
  16. calc_terminal/benchmark_system.py +414 -0
  17. calc_terminal/browser/__init__.py +36 -0
  18. calc_terminal/browser/browser_state.py +346 -0
  19. calc_terminal/browser/devserver.py +176 -0
  20. calc_terminal/browser/engine.py +494 -0
  21. calc_terminal/browser/navigation.py +84 -0
  22. calc_terminal/browser/preview.py +429 -0
  23. calc_terminal/browser/preview_entry.py +95 -0
  24. calc_terminal/browser/project_detector.py +144 -0
  25. calc_terminal/browser/server.py +449 -0
  26. calc_terminal/browser/state.py +75 -0
  27. calc_terminal/browser/watcher.py +99 -0
  28. calc_terminal/browser_gui/__init__.py +1 -0
  29. calc_terminal/browser_gui/__main__.py +3 -0
  30. calc_terminal/browser_gui/launcher.py +173 -0
  31. calc_terminal/browser_gui/playwright_browser.py +117 -0
  32. calc_terminal/browser_gui/qt_browser.py +1501 -0
  33. calc_terminal/browser_gui/webview_browser.py +57 -0
  34. calc_terminal/capabilities/__init__.py +35 -0
  35. calc_terminal/capabilities/adapters/__init__.py +33 -0
  36. calc_terminal/capabilities/adapters/bioinformatics.py +204 -0
  37. calc_terminal/capabilities/adapters/browser_adapter.py +205 -0
  38. calc_terminal/capabilities/adapters/filesystem.py +206 -0
  39. calc_terminal/capabilities/adapters/git_adapter.py +202 -0
  40. calc_terminal/capabilities/adapters/jupyter_adapter.py +138 -0
  41. calc_terminal/capabilities/adapters/ml_frameworks.py +158 -0
  42. calc_terminal/capabilities/adapters/platforms.py +200 -0
  43. calc_terminal/capabilities/adapters/python_exec.py +93 -0
  44. calc_terminal/capabilities/adapters/quantum_adapter.py +150 -0
  45. calc_terminal/capabilities/adapters/scientific_comp.py +123 -0
  46. calc_terminal/capabilities/adapters/structural_bio.py +161 -0
  47. calc_terminal/capabilities/adapters/terminal.py +99 -0
  48. calc_terminal/capabilities/bus.py +178 -0
  49. calc_terminal/capabilities/discovery.py +207 -0
  50. calc_terminal/capabilities/schema.py +221 -0
  51. calc_terminal/cat.ico +0 -0
  52. calc_terminal/cat_browser.py +2018 -0
  53. calc_terminal/chat_store.py +703 -0
  54. calc_terminal/cli.py +1178 -0
  55. calc_terminal/code_editor.py +640 -0
  56. calc_terminal/collaboration.py +723 -0
  57. calc_terminal/commands_data.py +139 -0
  58. calc_terminal/compatibility_engine.py +352 -0
  59. calc_terminal/compute/__init__.py +31 -0
  60. calc_terminal/compute/fabric.py +350 -0
  61. calc_terminal/config.py +227 -0
  62. calc_terminal/core/__init__.py +41 -0
  63. calc_terminal/core/checkpoint.py +156 -0
  64. calc_terminal/core/input/__init__.py +45 -0
  65. calc_terminal/core/mode_registry.py +300 -0
  66. calc_terminal/core/project_graph.py +172 -0
  67. calc_terminal/core/recovery.py +129 -0
  68. calc_terminal/core/security_layer.py +112 -0
  69. calc_terminal/core/task_graph.py +202 -0
  70. calc_terminal/core/unified_runtime.py +184 -0
  71. calc_terminal/core/verification.py +257 -0
  72. calc_terminal/customization.py +1566 -0
  73. calc_terminal/derivations.py +153 -0
  74. calc_terminal/device_control.py +263 -0
  75. calc_terminal/diagnostics/__init__.py +27 -0
  76. calc_terminal/diagnostics/doctor_engine.py +382 -0
  77. calc_terminal/diagnostics/self_test.py +247 -0
  78. calc_terminal/doctor.py +519 -0
  79. calc_terminal/easter_eggs.py +274 -0
  80. calc_terminal/editor/__init__.py +1 -0
  81. calc_terminal/editor/actions.py +263 -0
  82. calc_terminal/editor/commands.py +160 -0
  83. calc_terminal/editor/shortcuts.py +226 -0
  84. calc_terminal/engine.py +259 -0
  85. calc_terminal/errors.py +120 -0
  86. calc_terminal/event_stream.py +146 -0
  87. calc_terminal/eventbus.py +133 -0
  88. calc_terminal/extensions.py +733 -0
  89. calc_terminal/fallback_cli.py +1321 -0
  90. calc_terminal/first_run.py +265 -0
  91. calc_terminal/fomoji_auth.py +1043 -0
  92. calc_terminal/formulas.py +82 -0
  93. calc_terminal/fs_cache.py +121 -0
  94. calc_terminal/fs_watcher.py +277 -0
  95. calc_terminal/game.py +193 -0
  96. calc_terminal/gen1.py +5 -0
  97. calc_terminal/generators.py +245 -0
  98. calc_terminal/gestures/__init__.py +42 -0
  99. calc_terminal/gestures/bindings.py +175 -0
  100. calc_terminal/gestures/manager.py +477 -0
  101. calc_terminal/goodbye.py +363 -0
  102. calc_terminal/gpu3d.py +290 -0
  103. calc_terminal/graphs.py +358 -0
  104. calc_terminal/hardware_analyzer.py +440 -0
  105. calc_terminal/host/__init__.py +30 -0
  106. calc_terminal/host/browser_manager.py +187 -0
  107. calc_terminal/host/desktop.py +1386 -0
  108. calc_terminal/host/launcher.py +395 -0
  109. calc_terminal/host/terminal.py +279 -0
  110. calc_terminal/identity.py +216 -0
  111. calc_terminal/input/__init__.py +54 -0
  112. calc_terminal/input/capabilities.py +258 -0
  113. calc_terminal/input/focus.py +87 -0
  114. calc_terminal/input/gestures.py +64 -0
  115. calc_terminal/input/pointer.py +114 -0
  116. calc_terminal/input/touch.py +345 -0
  117. calc_terminal/keys.py +84 -0
  118. calc_terminal/live_automation.py +165 -0
  119. calc_terminal/mathtext.py +433 -0
  120. calc_terminal/mcp.py +386 -0
  121. calc_terminal/memory.py +337 -0
  122. calc_terminal/memory_v2.py +479 -0
  123. calc_terminal/metrics.py +333 -0
  124. calc_terminal/mode_detection.py +146 -0
  125. calc_terminal/model.py +2431 -0
  126. calc_terminal/model_router.py +665 -0
  127. calc_terminal/models/__init__.py +0 -0
  128. calc_terminal/models/active_state.py +187 -0
  129. calc_terminal/models/dynamic_registry.py +584 -0
  130. calc_terminal/models/manager.py +781 -0
  131. calc_terminal/models/model_metadata.json +3526 -0
  132. calc_terminal/models/profiles.py +194 -0
  133. calc_terminal/models/registry.py +265 -0
  134. calc_terminal/models/schema.py +197 -0
  135. calc_terminal/models/validator.py +287 -0
  136. calc_terminal/models/verification_engine.py +368 -0
  137. calc_terminal/native_picker.py +215 -0
  138. calc_terminal/ollama_catalog.py +279 -0
  139. calc_terminal/ollama_download.py +233 -0
  140. calc_terminal/orchestrator.py +304 -0
  141. calc_terminal/package_research.py +322 -0
  142. calc_terminal/packages.py +1024 -0
  143. calc_terminal/pc_specs.py +116 -0
  144. calc_terminal/permissions.py +334 -0
  145. calc_terminal/pet.py +106 -0
  146. calc_terminal/pipeline.py +505 -0
  147. calc_terminal/platform/__init__.py +491 -0
  148. calc_terminal/platform/desktop.py +491 -0
  149. calc_terminal/platform/web.py +781 -0
  150. calc_terminal/preview/__init__.py +1 -0
  151. calc_terminal/preview/dev_server.py +303 -0
  152. calc_terminal/preview/diagnostics.py +131 -0
  153. calc_terminal/preview/live_reload.py +66 -0
  154. calc_terminal/preview/manager.py +129 -0
  155. calc_terminal/project_stats.py +209 -0
  156. calc_terminal/projects.py +328 -0
  157. calc_terminal/providers/__init__.py +0 -0
  158. calc_terminal/providers/adapters/__init__.py +80 -0
  159. calc_terminal/providers/adapters/anthropic_adapter.py +127 -0
  160. calc_terminal/providers/adapters/base.py +106 -0
  161. calc_terminal/providers/adapters/chinese_adapters.py +420 -0
  162. calc_terminal/providers/adapters/gemini_adapter.py +101 -0
  163. calc_terminal/providers/adapters/ollama_adapter.py +83 -0
  164. calc_terminal/providers/adapters/openai_adapter.py +159 -0
  165. calc_terminal/providers/adapters/other_adapters.py +246 -0
  166. calc_terminal/providers/anthropic_provider.py +172 -0
  167. calc_terminal/providers/auto_update.py +416 -0
  168. calc_terminal/providers/base_provider.py +105 -0
  169. calc_terminal/providers/discovery_manager.py +207 -0
  170. calc_terminal/providers/gemini_provider.py +178 -0
  171. calc_terminal/providers/lifecycle.py +767 -0
  172. calc_terminal/providers/ollama_adapter.py +707 -0
  173. calc_terminal/providers/openai_provider.py +254 -0
  174. calc_terminal/providers/provider_manager.py +1827 -0
  175. calc_terminal/providers/providers.json +4075 -0
  176. calc_terminal/reactionsim.py +279 -0
  177. calc_terminal/registry.py +337 -0
  178. calc_terminal/report.py +162 -0
  179. calc_terminal/research/__init__.py +45 -0
  180. calc_terminal/research/artifact_intel.py +126 -0
  181. calc_terminal/research/data_lineage.py +123 -0
  182. calc_terminal/research/experiment_ledger.py +303 -0
  183. calc_terminal/research/reproducibility.py +131 -0
  184. calc_terminal/resilience/__init__.py +47 -0
  185. calc_terminal/resilience/agent_state.py +121 -0
  186. calc_terminal/resilience/capability_matcher.py +174 -0
  187. calc_terminal/resilience/circuit_breaker.py +158 -0
  188. calc_terminal/resilience/failover_engine.py +230 -0
  189. calc_terminal/resilience/health_monitor.py +192 -0
  190. calc_terminal/resilience/ollama_adapter.py +125 -0
  191. calc_terminal/resilience/orchestrator.py +312 -0
  192. calc_terminal/resilience/types.py +134 -0
  193. calc_terminal/sandbox.py +98 -0
  194. calc_terminal/scires.py +558 -0
  195. calc_terminal/security_scanner.py +126 -0
  196. calc_terminal/session.py +294 -0
  197. calc_terminal/sim3d.py +206 -0
  198. calc_terminal/solver.py +276 -0
  199. calc_terminal/sound.py +127 -0
  200. calc_terminal/task_reports.py +287 -0
  201. calc_terminal/terminal_host.py +201 -0
  202. calc_terminal/terminal_identity.py +411 -0
  203. calc_terminal/test_ai_mode_reliability.py +344 -0
  204. calc_terminal/test_browser.py +368 -0
  205. calc_terminal/test_code_editor_upgrade.py +485 -0
  206. calc_terminal/test_customization.py +1148 -0
  207. calc_terminal/test_customization_ui.py +612 -0
  208. calc_terminal/test_dynamic_registry.py +304 -0
  209. calc_terminal/test_extensions.py +436 -0
  210. calc_terminal/test_overhaul.py +557 -0
  211. calc_terminal/test_project_detect.py +255 -0
  212. calc_terminal/test_root_cause_fix.py +527 -0
  213. calc_terminal/test_stability.py +532 -0
  214. calc_terminal/test_terminal_identity.py +132 -0
  215. calc_terminal/test_v079_speed.py +460 -0
  216. calc_terminal/theme.py +1107 -0
  217. calc_terminal/timeline.py +139 -0
  218. calc_terminal/todos.py +246 -0
  219. calc_terminal/tool_call_normalizer.py +419 -0
  220. calc_terminal/tui.py +104 -0
  221. calc_terminal/ui/__init__.py +8 -0
  222. calc_terminal/ui/activity_panel.py +231 -0
  223. calc_terminal/ui/activity_stream_panel.py +238 -0
  224. calc_terminal/ui/animations.py +122 -0
  225. calc_terminal/ui/app.py +7271 -0
  226. calc_terminal/ui/attach_panel.py +597 -0
  227. calc_terminal/ui/attachments.py +424 -0
  228. calc_terminal/ui/backup_panel.py +810 -0
  229. calc_terminal/ui/browser_shell.py +887 -0
  230. calc_terminal/ui/cat_agent.py +357 -0
  231. calc_terminal/ui/chats_panel.py +899 -0
  232. calc_terminal/ui/command_palette.py +125 -0
  233. calc_terminal/ui/command_palette_modal.py +166 -0
  234. calc_terminal/ui/composer.py +1141 -0
  235. calc_terminal/ui/context_menu.py +197 -0
  236. calc_terminal/ui/conversation.py +1435 -0
  237. calc_terminal/ui/customization_panel.py +1229 -0
  238. calc_terminal/ui/dashboard.py +404 -0
  239. calc_terminal/ui/design_system.py +557 -0
  240. calc_terminal/ui/diff_panel.py +213 -0
  241. calc_terminal/ui/editor.py +2102 -0
  242. calc_terminal/ui/empty_state.py +302 -0
  243. calc_terminal/ui/events.py +487 -0
  244. calc_terminal/ui/extensions_panel.py +815 -0
  245. calc_terminal/ui/footer.py +166 -0
  246. calc_terminal/ui/gestures_panel.py +383 -0
  247. calc_terminal/ui/goodbye_screen.py +100 -0
  248. calc_terminal/ui/header.py +1034 -0
  249. calc_terminal/ui/help_panel.py +254 -0
  250. calc_terminal/ui/live_activities.py +914 -0
  251. calc_terminal/ui/mcp_panel.py +570 -0
  252. calc_terminal/ui/memory_center.py +524 -0
  253. calc_terminal/ui/mode_colors_panel.py +525 -0
  254. calc_terminal/ui/nav_screens.py +747 -0
  255. calc_terminal/ui/ollama_panel.py +536 -0
  256. calc_terminal/ui/palette.py +221 -0
  257. calc_terminal/ui/permission_panel.py +269 -0
  258. calc_terminal/ui/personalization_panel.py +517 -0
  259. calc_terminal/ui/personalize_center.py +1568 -0
  260. calc_terminal/ui/preview_panel.py +441 -0
  261. calc_terminal/ui/resizers.py +402 -0
  262. calc_terminal/ui/sidebar.py +1285 -0
  263. calc_terminal/ui/statusbar.py +168 -0
  264. calc_terminal/ui/theme_css.py +1396 -0
  265. calc_terminal/ui/thinking.py +226 -0
  266. calc_terminal/ui/timeline_panel.py +102 -0
  267. calc_terminal/ui/todo_panel.py +193 -0
  268. calc_terminal/ui/viewport.py +136 -0
  269. calc_terminal/ui/vision_panel.py +489 -0
  270. calc_terminal/ui/welcome_modal.py +343 -0
  271. calc_terminal/ui/widgets.py +160 -0
  272. calc_terminal/ui/workspace.py +831 -0
  273. calc_terminal/viewers/__init__.py +1 -0
  274. calc_terminal/viewers/document_viewer.py +252 -0
  275. calc_terminal/viewers/image_viewer.py +241 -0
  276. calc_terminal/viewers/pdf_viewer.py +203 -0
  277. calc_terminal/viewers/presentation_viewer.py +164 -0
  278. calc_terminal/viewers/registry.py +120 -0
  279. calc_terminal/viewers/spreadsheet_viewer.py +204 -0
  280. calc_terminal/vision/__init__.py +89 -0
  281. calc_terminal/vision/analysis.py +194 -0
  282. calc_terminal/vision/annotations.py +297 -0
  283. calc_terminal/vision/capture.py +171 -0
  284. calc_terminal/vision/context.py +231 -0
  285. calc_terminal/vision/correlation.py +169 -0
  286. calc_terminal/vision/cursor.py +258 -0
  287. calc_terminal/vision/events.py +66 -0
  288. calc_terminal/vision/frame_pipeline.py +259 -0
  289. calc_terminal/vision/priority.py +218 -0
  290. calc_terminal/vision/provider.py +180 -0
  291. calc_terminal/vision/safety.py +149 -0
  292. calc_terminal/vision/session.py +281 -0
  293. calc_terminal/vision/verify.py +162 -0
  294. calc_terminal/vision.py +514 -0
  295. calc_terminal/vscode_integration.py +113 -0
  296. calc_terminal/web/__init__.py +8 -0
  297. calc_terminal/web/cat_runtime.py +710 -0
  298. calc_terminal/web/server.py +2891 -0
  299. calc_terminal/web/static/css/app.css +3152 -0
  300. calc_terminal/web/static/icons/badge-72.png +0 -0
  301. calc_terminal/web/static/icons/cat.ico +0 -0
  302. calc_terminal/web/static/icons/icon-128.png +0 -0
  303. calc_terminal/web/static/icons/icon-144.png +0 -0
  304. calc_terminal/web/static/icons/icon-152.png +0 -0
  305. calc_terminal/web/static/icons/icon-192.png +0 -0
  306. calc_terminal/web/static/icons/icon-384.png +0 -0
  307. calc_terminal/web/static/icons/icon-512.png +0 -0
  308. calc_terminal/web/static/icons/icon-72.png +0 -0
  309. calc_terminal/web/static/icons/icon-96.png +0 -0
  310. calc_terminal/web/static/icons/icon.svg +34 -0
  311. calc_terminal/web/static/icons/new-project.png +0 -0
  312. calc_terminal/web/static/icons/open-project.png +0 -0
  313. calc_terminal/web/static/index.html +734 -0
  314. calc_terminal/web/static/js/app.js +2403 -0
  315. calc_terminal/web/static/manifest.json +88 -0
  316. calc_terminal/web/static/sw.js +230 -0
  317. calc_terminal/workflow_engine.py +769 -0
  318. calc_terminal/workspace.py +593 -0
  319. calc_terminal/workspace_index.py +385 -0
  320. cct_cli-0.7.9.0.dist-info/METADATA +210 -0
  321. cct_cli-0.7.9.0.dist-info/RECORD +325 -0
  322. cct_cli-0.7.9.0.dist-info/WHEEL +5 -0
  323. cct_cli-0.7.9.0.dist-info/entry_points.txt +4 -0
  324. cct_cli-0.7.9.0.dist-info/licenses/LICENSE +21 -0
  325. cct_cli-0.7.9.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,505 @@
1
+ """
2
+ CCT — pipeline.py: the AI Execution Pipeline (v0.7.7 spec section 6/7/8).
3
+
4
+ Large tasks automatically follow the spec's workflow, and the workflow
5
+ is VISIBLE to the user the whole way:
6
+
7
+ Step 1 Planner Mode -> analyze request, break into milestones,
8
+ generate roadmap + task graph + todos,
9
+ estimate complexity
10
+ (auto switch)
11
+ Step 2 Research Mode -> search documentation, read APIs, compare
12
+ technologies, deep reasoning
13
+ (auto switch)
14
+ Step 3 Build Mode -> create files, install packages, generate
15
+ code, run tests, fix issues, verify output
16
+ (auto switch)
17
+ Step 4 Notebook Mode -> report, flowchart, architecture diagram,
18
+ performance summary, final explanation
19
+
20
+ Implementation notes (honest scope):
21
+ * Stages are real, sequential passes over the configured AI provider
22
+ (the same single-provider architecture agent.py's multi-agent team
23
+ uses — see its "Multi-agent collaboration" section). Planner and
24
+ Tester are dedicated prompt passes; Implementation reuses
25
+ agent.run_agent()'s real tool-executing loop so files/plots/
26
+ installs genuinely happen; the Debugger pass reuses agent._debug_pass
27
+ (real ast scan + sandbox run).
28
+ * The live todo list (spec section 7) drives the stage UI: the plan
29
+ becomes todos with statuses Pending / Researching / Coding /
30
+ Testing / Completed / Skipped / Failed / Currently Running. Status
31
+ transitions are published through the on_todo callback and the
32
+ eventbus so the Todo panel and the task graph can both react.
33
+ * "Running" is the current stage's highlight (spec section 8) — the
34
+ graph text below is rendered by callers; this module just tracks
35
+ which stage is current and what happened at each.
36
+ * Complexity estimation is a heuristic (word count + plan length),
37
+ labeled as such in the report — not a model call.
38
+
39
+ Checkpoints (v0.7.8.1): after every completed stage the run state
40
+ (plan, todos, steps, research brief, stage) is persisted to
41
+ ~/.cct_pipeline_checkpoint.json. When a stage fails — say the provider
42
+ chain is exhausted mid-run — the caller can run again with
43
+ resume=True: matching runs skip the completed stages and continue
44
+ from the failed one instead of restarting from the planner. The
45
+ checkpoint is cleared once a run reaches 'completed'. This is the
46
+ long-task "continue from checkpoint" + provider-failover-resume
47
+ behavior, done with real persisted state, not a fiction.
48
+ """
49
+
50
+ if __name__ == "__main__":
51
+ print("This is a library file and is not meant to be run directly.")
52
+ import sys
53
+ sys.exit(1)
54
+
55
+ import json
56
+ import os
57
+ import re
58
+ import time
59
+
60
+ from . import aicore
61
+ from . import agent
62
+ from . import eventbus
63
+ from . import sound
64
+
65
+ # ------------------------------------------------------------------ stages --
66
+ PIPELINE_STAGES = [
67
+ "planner", # Step 1 — Plan
68
+ "research", # Step 2 — Research
69
+ "implementation", # Step 3 — Build
70
+ "testing", # Step 3b — Test & fix
71
+ "verification", # Step 3c — Verify
72
+ "documentation", # Step 4 — Notebook report
73
+ "completed",
74
+ ]
75
+
76
+ _STAGE_ORDER = [
77
+ "planner",
78
+ "research",
79
+ "implementation",
80
+ "testing",
81
+ "verification",
82
+ "documentation",
83
+ "completed",
84
+ ]
85
+
86
+ _CHECKPOINT_FILE = os.path.join(os.path.expanduser("~"), ".cct_pipeline_checkpoint.json")
87
+
88
+ STAGE_LABELS = {
89
+ "planner": "\U0001f9ed Planner",
90
+ "research": "\U0001f50d Research",
91
+ "implementation": "\U0001f528 Implementation",
92
+ "testing": "\u2699 Testing",
93
+ "verification": "\u2705 Verification",
94
+ "documentation": "\U0001f4d8 Documentation",
95
+ "completed": "\U0001f389 Completed",
96
+ }
97
+
98
+ # Live-todo status vocabulary (spec section 7) — mirrors the statuses
99
+ # the Todo panel renders.
100
+ TODO_PENDING = "pending"
101
+ TODO_RUNNING = "running" # current stage in flight
102
+ TODO_RESEARCHING = "researching"
103
+ TODO_CODING = "coding"
104
+ TODO_TESTING = "testing"
105
+ TODO_COMPLETED = "completed"
106
+ TODO_SKIPPED = "skipped"
107
+ TODO_FAILED = "failed"
108
+
109
+ _ANY_STAGE = re.compile(r"\b(planner|planning|research|implementation|build|testing|verify|verification|documentation|docs)\b", re.I)
110
+
111
+
112
+ def _classify_stage(chunk_text):
113
+ """Very cheap keyword stage classifier for the tool-step -> stage
114
+ mapping during Implementation (see _PipelineRun._advance_todos)."""
115
+ t = (chunk_text or "").lower()
116
+ if re.search(r"\b(test|pytest|assert|sandbox)\b", t):
117
+ return "testing"
118
+ if re.search(r"\b(search|research|docs|documentation)\b", t):
119
+ return "research"
120
+ if re.search(r"\b(solve|plot|simulate|calculate|write_file|create_folder)\b", t):
121
+ return "coding"
122
+ return None
123
+
124
+
125
+ def _classify_intent(text):
126
+ """Best-effort complexity + stage-intent estimate for the report
127
+ (spec section 6's "estimate complexity"). Heuristic, labeled as
128
+ such in the report text."""
129
+ words = len(re.findall(r"\w+", text or ""))
130
+ if words < 20:
131
+ return "simple"
132
+ if words < 60:
133
+ return "moderate"
134
+ return "complex"
135
+
136
+
137
+ # ---------------------------------------------------------------- graph --
138
+ def task_graph(current_stage=None):
139
+ """The execution graph (spec section 8): planner -> research ->
140
+ implementation -> testing -> verification -> documentation ->
141
+ completed, with the current stage highlighted. Returns a list of
142
+ (stage_key, label, is_current) tuples so callers can render it
143
+ with their own theming."""
144
+ out = []
145
+ for stage in PIPELINE_STAGES:
146
+ out.append((stage, STAGE_LABELS[stage], stage == current_stage))
147
+ return out
148
+
149
+
150
+ def render_task_graph(current_stage, term_width=78):
151
+ """Classic-terminal render of the task graph, current stage
152
+ highlighted with a marker."""
153
+ from . import theme
154
+ lines = []
155
+ for i, (stage, label, current) in enumerate(task_graph(current_stage)):
156
+ arrow = "\u2193"
157
+ if stage == "planner":
158
+ prefix = ""
159
+ else:
160
+ prefix = " "
161
+ indent = " " * 2
162
+ if current:
163
+ lines.append(f"{indent}{theme.cyan(arrow if stage != 'planner' else ' ')} "
164
+ f"{theme.cyan('>', bold=True)} {theme.cyan(label, bold=True)} {theme.orange('\u25cf CURRENT', bold=True)}")
165
+ else:
166
+ lines.append(f"{indent}{theme.faint(arrow if stage != 'planner' else ' ')} {theme.faint(label)}")
167
+ return lines
168
+
169
+
170
+ # ---------------------------------------------------------- checkpoints --
171
+ def save_checkpoint(run) -> bool:
172
+ """Persist a pipeline run's state at its current stage so a later
173
+ run with the same user_text can resume from where it stopped."""
174
+ try:
175
+ payload = {
176
+ "user_text": run.user_text,
177
+ "mode": run.mode,
178
+ "last_completed": run._last_completed,
179
+ "plan": run.plan,
180
+ "todos": run.todos,
181
+ "steps": run.steps,
182
+ "final_text": run.final_text,
183
+ "research_brief": getattr(run, "research_brief", ""),
184
+ "started": run.started,
185
+ }
186
+ with open(_CHECKPOINT_FILE, "w", encoding="utf-8") as f:
187
+ json.dump(payload, f, indent=2)
188
+ return True
189
+ except Exception:
190
+ return False
191
+
192
+
193
+ def load_checkpoint() -> dict | None:
194
+ try:
195
+ if os.path.exists(_CHECKPOINT_FILE):
196
+ with open(_CHECKPOINT_FILE, "r", encoding="utf-8") as f:
197
+ data = json.load(f)
198
+ if isinstance(data, dict) and "last_completed" in data:
199
+ return data
200
+ except Exception:
201
+ pass
202
+ return None
203
+
204
+
205
+ def clear_checkpoint():
206
+ try:
207
+ if os.path.exists(_CHECKPOINT_FILE):
208
+ os.remove(_CHECKPOINT_FILE)
209
+ except Exception:
210
+ pass
211
+
212
+
213
+ def has_checkpoint(user_text) -> bool:
214
+ cp = load_checkpoint()
215
+ return bool(cp and cp.get("user_text") == user_text)
216
+
217
+
218
+ # ------------------------------------------------------------------ run --
219
+ class PipelineRun:
220
+ """One full pipeline execution. Callbacks (all optional, all plain
221
+ Python — safe from any thread):
222
+ on_note(text) — a one-line status note (stage entered)
223
+ on_stage(stage_key) — stage transition happened
224
+ on_todo(todo_dict) — a todo's status changed
225
+ on_message(text) — free-form text to surface (report lines)
226
+ on_event(ev_dict) — raw pipeline event (for logs/dashboards)
227
+ """
228
+
229
+ def __init__(self, user_text, mode="agent", on_note=None, on_stage=None,
230
+ on_todo=None, on_message=None, on_event=None, permission_callback=None,
231
+ should_cancel=None):
232
+ self.user_text = user_text
233
+ self.mode = mode
234
+ self.on_note = on_note or (lambda t: None)
235
+ self.on_stage = on_stage or (lambda s: None)
236
+ self.on_todo = on_todo or (lambda t: None)
237
+ self.on_message = on_message or (lambda t: None)
238
+ self.on_event = on_event or (lambda e: None)
239
+ self.permission_callback = permission_callback
240
+ # v0.7.8.1 interrupt: zero-arg callable checked before each stage
241
+ # and threaded into the implementation stage's agent loop.
242
+ self.should_cancel = should_cancel
243
+ self.stage = None
244
+ self.todos = [] # [{id, text, status}]
245
+ self.steps = [] # agent tool steps
246
+ self.plan = [] # planner step list
247
+ self.report_lines = [] # documentation stage output
248
+ self.final_text = ""
249
+ self.started = time.time()
250
+ self.duration = 0.0
251
+ self.failed_stage = None
252
+ self._todo_seq = 0
253
+ self._last_completed = None # last stage that finished cleanly
254
+
255
+ # ------------------------------------------------------------ helpers --
256
+ def _cancelled(self):
257
+ """v0.7.8.1 interrupt: True once the caller's cancel callback
258
+ fires (the UI worker marks its worker cancelled on Stop/Esc)."""
259
+ try:
260
+ return bool(self.should_cancel and self.should_cancel())
261
+ except Exception:
262
+ return False
263
+
264
+ def _restore_checkpoint(self):
265
+ """Load the saved checkpoint for THIS user_text and fast-forward
266
+ the run state to just after the last completed stage. Returns
267
+ True when a usable checkpoint was found."""
268
+ cp = load_checkpoint()
269
+ if not cp or cp.get("user_text") != self.user_text:
270
+ return False
271
+ self.plan = cp.get("plan") or []
272
+ self.todos = cp.get("todos") or []
273
+ self.steps = cp.get("steps") or []
274
+ self.final_text = cp.get("final_text") or ""
275
+ self.research_brief = cp.get("research_brief", "")
276
+ self.started = cp.get("started") or time.time()
277
+ self._last_completed = cp.get("last_completed") or None
278
+ self._todo_seq = max((t.get("id", 0) for t in self.todos), default=0)
279
+ for t in self.todos:
280
+ self.on_todo(t) # re-sync live todo UIs to restored statuses
281
+ return True
282
+ def _emit(self, kind, message, **extra):
283
+ ev = {"kind": kind, "stage": self.stage, "message": message}
284
+ ev.update(extra)
285
+ self.on_event(ev)
286
+ if kind in ("note", "stage", "error", "done"):
287
+ self.on_note(message)
288
+ elif kind in ("report", "summary", "graph"):
289
+ self.on_message(message)
290
+
291
+ def _enter(self, stage):
292
+ self.stage = stage
293
+ self.on_stage(stage)
294
+ eventbus.bus.publish(eventbus.PIPELINE_STAGE, stage=stage,
295
+ detail=STAGE_LABELS.get(stage, stage))
296
+ if stage == "testing":
297
+ for t in self.todos:
298
+ if t["status"] not in (TODO_COMPLETED, TODO_SKIPPED, TODO_FAILED):
299
+ t["status"] = TODO_TESTING
300
+ self.on_todo(t)
301
+ elif stage == "research":
302
+ for t in self.todos:
303
+ if t["status"] not in (TODO_COMPLETED, TODO_SKIPPED, TODO_FAILED):
304
+ t["status"] = TODO_RESEARCHING
305
+ self.on_todo(t)
306
+ elif stage == "implementation":
307
+ for t in self.todos:
308
+ if t["status"] not in (TODO_COMPLETED, TODO_SKIPPED, TODO_FAILED):
309
+ t["status"] = TODO_RUNNING
310
+ self.on_todo(t)
311
+
312
+ def _add_todo(self, text):
313
+ self._todo_seq += 1
314
+ todo = {"id": self._todo_seq, "text": text, "status": TODO_PENDING}
315
+ self.todos.append(todo)
316
+ self.on_todo(todo)
317
+ return todo
318
+
319
+ def _advance_todos(self, step_text):
320
+ """Best-effort mapping of executed tool steps onto the plan's
321
+ todos during Implementation: each tool step completes the first
322
+ not-yet-done todo. Documented as an approximation, not a
323
+ semantic match."""
324
+ if not self.todos:
325
+ return
326
+ for t in self.todos:
327
+ if t["status"] not in (TODO_COMPLETED, TODO_SKIPPED, TODO_FAILED):
328
+ t["status"] = TODO_COMPLETED
329
+ self.on_todo(t)
330
+ return
331
+
332
+ # ------------------------------------------------------------ stages --
333
+ def _stage_planner(self):
334
+ """Step 1 — Planner Mode: analyze, break into milestones,
335
+ roadmap, todos, complexity estimate."""
336
+ self._emit("note", "Step 1/6 \u2014 Planner: analyzing the request and breaking it into milestones...")
337
+ system_prompt = agent.AGENT_SYSTEM_PROMPT if self.mode == "agent" else agent.AI_SYSTEM_PROMPT
338
+ plan = agent._plan_steps(self.user_text, system_prompt)
339
+ self.plan = plan
340
+ for step in plan:
341
+ self._add_todo(step)
342
+ if not self.plan:
343
+ self._add_todo("Work out and answer the request directly.")
344
+ complexity = _classify_intent(self.user_text)
345
+ self._emit("note",
346
+ f"Planner produced {len(self.plan)} milestone(s) \u00b7 "
347
+ f"complexity estimate: {complexity} (heuristic)")
348
+
349
+ def _stage_research(self):
350
+ """Step 2 — Research Mode: web brief when the request has a
351
+ recency/lookup flavor, folded into the Implementation context."""
352
+ self._emit("note", "Step 2/6 \u2014 Research: searching documentation and comparing approaches...")
353
+ brief = agent._research_pass(self.user_text, agent.AGENT_SYSTEM_PROMPT)
354
+ self.research_brief = brief
355
+ if brief:
356
+ self._emit("note", "Research brief gathered (live web search) \u2014 folding into the build.")
357
+ else:
358
+ self._emit("note", "No live research needed for this request (or search unreachable).")
359
+
360
+ def _stage_implementation(self):
361
+ """Step 3 — Build Mode: real tool execution (files, installs,
362
+ plots) via agent.run_agent."""
363
+ self._emit("note", "Step 3/6 \u2014 Implementation: creating files, installing packages, generating code...")
364
+ plan_note = ("\n\nPIPELINE PLAN (follow it step by step, completing each "
365
+ "milestone in order):\n" +
366
+ "\n".join(f"{i+1}. {p}" for i, p in enumerate(self.plan)))
367
+ if getattr(self, "research_brief", ""):
368
+ plan_note += "\n\n" + self.research_brief
369
+
370
+ def _on_step(name, args):
371
+ args = args or {}
372
+ target = (args.get("path") or args.get("command") or "").strip()
373
+ label = f"{name}" + (f" {target}" if target else "")
374
+ self._emit("note", f"\u2699 executing: {label}")
375
+ self._advance_todos(label)
376
+
377
+ final_text, steps, _meta = agent.run_agent(
378
+ self.user_text + plan_note, max_steps=agent.MAX_STEPS,
379
+ verbose=False, mode=self.mode,
380
+ permission_callback=self.permission_callback,
381
+ on_step=_on_step, should_cancel=self._cancelled)
382
+ self.steps = steps
383
+ self.final_text = final_text
384
+ changes = agent.summarize_file_changes(steps)
385
+ if changes:
386
+ self._emit("report", changes)
387
+ self._emit("note", f"Implementation finished \u2014 {len(steps)} tool step(s) run.")
388
+
389
+ def _stage_testing(self):
390
+ """Step 3b — Debugger/Tester pass: static scan + sandbox run of
391
+ any code blocks the implementation produced."""
392
+ self._emit("note", "Step 4/6 \u2014 Testing: statically scanning and sandbox-testing generated code...")
393
+ final_text, note = agent._debug_pass(self.final_text)
394
+ if note:
395
+ risk = note.get("risk", "none")
396
+ self._emit("note",
397
+ f"Debugger scan: {risk} risk "
398
+ + (f"\u2014 {note.get('sandbox_test', 'not sandboxed')}" if risk != "high" else
399
+ "\u2014 high-risk code not auto-executed"))
400
+ if risk == "high":
401
+ for t in self.todos:
402
+ if t["status"] == TODO_TESTING:
403
+ pass
404
+ if note.get("findings"):
405
+ details = "\n".join(f" {f}" for f in note["findings"][:5])
406
+ self._emit("report", "Debugger findings:\n" + details)
407
+ self.final_text = final_text
408
+
409
+ def _stage_verification(self):
410
+ """Step 3c — Tester pass: spot-check the final answer."""
411
+ self._emit("note", "Step 5/6 \u2014 Verification: checking the result against the request...")
412
+ ok, note = agent._verify_answer(self.user_text, self.plan, self.final_text,
413
+ agent.AGENT_SYSTEM_PROMPT)
414
+ if not ok and note:
415
+ self._emit("note", f"Tester flagged this for review: {note}")
416
+ self.final_text = self.final_text + f"\n\n\u26a0 Tester flagged this for review: {note}"
417
+ else:
418
+ self._emit("note", "Verification passed \u2014 the result is internally consistent.")
419
+
420
+ def _stage_documentation(self):
421
+ """Step 4 — Notebook Mode: the final report card (stages,
422
+ graph, todos, summary)."""
423
+ self._emit("note", "Step 6/6 \u2014 Documentation: composing the final report...")
424
+ self._emit("graph", "Pipeline task graph (current stage highlighted):")
425
+ graph_lines = render_task_graph("completed")
426
+ self._emit("report", "\n".join(graph_lines))
427
+ done = sum(1 for t in self.todos if t["status"] == TODO_COMPLETED)
428
+ skipped = sum(1 for t in self.todos if t["status"] == TODO_SKIPPED)
429
+ failed = sum(1 for t in self.todos if t["status"] == TODO_FAILED)
430
+ self.duration = round(time.time() - self.started, 1)
431
+ card = [
432
+ "\u2705 Pipeline complete",
433
+ "",
434
+ f"Milestones: {len(self.plan)} \u00b7 completed {done}"
435
+ + (f" \u00b7 skipped {skipped}" if skipped else "")
436
+ + (f" \u00b7 failed {failed}" if failed else ""),
437
+ f"Tool steps executed: {len(self.steps)}",
438
+ f"Duration: {self.duration}s",
439
+ f"Complexity estimate: {_classify_intent(self.user_text)} (heuristic)",
440
+ ]
441
+ self._emit("summary", "\n".join(card))
442
+ eventbus.bus.publish(eventbus.PIPELINE_COMPLETED, steps=len(self.steps),
443
+ milestones=len(self.plan))
444
+
445
+ # --------------------------------------------------------------- run --
446
+ def run(self, resume=False):
447
+ """Execute the pipeline. Stages run in order; after each one
448
+ completes the state is checkpointed (so a failed run can be
449
+ resumed). With `resume=True`, a matching checkpoint is loaded
450
+ and the completed stages are skipped — execution continues
451
+ from the last-completed stage. The checkpoint is cleared when
452
+ the run reaches 'completed'."""
453
+ sound.play("notify")
454
+ try:
455
+ resumed = False
456
+ if resume:
457
+ resumed = self._restore_checkpoint()
458
+ if resumed:
459
+ at = STAGE_LABELS.get(self._last_completed, self._last_completed or "start")
460
+ self._emit("note",
461
+ f"\u267b Resuming from checkpoint \u2014 "
462
+ f"skipping completed stages (up to '{at}').")
463
+ else:
464
+ self._emit("note", "No matching checkpoint found \u2014 starting fresh.")
465
+ else:
466
+ clear_checkpoint() # stale checkpoint from a previous crash
467
+ skip_until = self._last_completed if resumed else None
468
+ stages = [
469
+ ("planner", self._stage_planner),
470
+ ("research", self._stage_research),
471
+ ("implementation", self._stage_implementation),
472
+ ("testing", self._stage_testing),
473
+ ("verification", self._stage_verification),
474
+ ("documentation", self._stage_documentation),
475
+ ]
476
+ skipping = False
477
+ for key, fn in stages:
478
+ if skip_until and not skipping:
479
+ skipping = (key == skip_until)
480
+ continue # completed in the previous run — skip it too
481
+ if self._cancelled():
482
+ self._emit("note",
483
+ "\u23f9 Interrupted \u2014 completed work is preserved.")
484
+ return self.final_text
485
+ self._enter(key)
486
+ fn()
487
+ self._last_completed = key
488
+ save_checkpoint(self)
489
+ self._enter("completed")
490
+ sound.play("success")
491
+ clear_checkpoint()
492
+ except Exception as e:
493
+ self.failed_stage = self.stage
494
+ self._emit("error", f"Pipeline failed at {STAGE_LABELS.get(self.stage, self.stage)}: {e}")
495
+ save_checkpoint(self)
496
+ sound.play("error")
497
+ return self.final_text
498
+
499
+
500
+ def run_pipeline(user_text, **callbacks):
501
+ """Module-level convenience: builds a PipelineRun, runs it, returns
502
+ it (callers read .final_text / .todos / .plan / .duration)."""
503
+ run = PipelineRun(user_text, **callbacks)
504
+ run.run()
505
+ return run