vidbyte-cli 0.1.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 (269) hide show
  1. vidbyte_cli/__init__.py +11 -0
  2. vidbyte_cli/__main__.py +6 -0
  3. vidbyte_cli/cli.py +18 -0
  4. vidbyte_cli/commands/__init__.py +86 -0
  5. vidbyte_cli/commands/agent_options.py +53 -0
  6. vidbyte_cli/commands/agents/__init__.py +31 -0
  7. vidbyte_cli/commands/agents/suggestion/__init__.py +38 -0
  8. vidbyte_cli/commands/agents/suggestion/prompts/__init__.py +3 -0
  9. vidbyte_cli/commands/agents/suggestion/prompts/all_categories.md +5 -0
  10. vidbyte_cli/commands/agents/suggestion/prompts/approaches.md +100 -0
  11. vidbyte_cli/commands/agents/suggestion/prompts/avoid.md +100 -0
  12. vidbyte_cli/commands/agents/suggestion/prompts/blockers.md +100 -0
  13. vidbyte_cli/commands/agents/suggestion/prompts/capability.md +100 -0
  14. vidbyte_cli/commands/agents/suggestion/prompts/categories.md +5 -0
  15. vidbyte_cli/commands/agents/suggestion/prompts/category.md +5 -0
  16. vidbyte_cli/commands/agents/suggestion/prompts/completed.md +100 -0
  17. vidbyte_cli/commands/agents/suggestion/prompts/constraint.md +100 -0
  18. vidbyte_cli/commands/agents/suggestion/prompts/context.md +100 -0
  19. vidbyte_cli/commands/agents/suggestion/prompts/count.md +5 -0
  20. vidbyte_cli/commands/agents/suggestion/prompts/critic_model.md +100 -0
  21. vidbyte_cli/commands/agents/suggestion/prompts/decision.md +100 -0
  22. vidbyte_cli/commands/agents/suggestion/prompts/dry_run.md +5 -0
  23. vidbyte_cli/commands/agents/suggestion/prompts/extra_compute.md +5 -0
  24. vidbyte_cli/commands/agents/suggestion/prompts/feedback_accept.md +7 -0
  25. vidbyte_cli/commands/agents/suggestion/prompts/feedback_group.md +7 -0
  26. vidbyte_cli/commands/agents/suggestion/prompts/feedback_project.md +100 -0
  27. vidbyte_cli/commands/agents/suggestion/prompts/feedback_reason.md +98 -0
  28. vidbyte_cli/commands/agents/suggestion/prompts/feedback_reject.md +7 -0
  29. vidbyte_cli/commands/agents/suggestion/prompts/feedback_suggestion.md +101 -0
  30. vidbyte_cli/commands/agents/suggestion/prompts/files.md +100 -0
  31. vidbyte_cli/commands/agents/suggestion/prompts/forbidden.md +100 -0
  32. vidbyte_cli/commands/agents/suggestion/prompts/goal.md +100 -0
  33. vidbyte_cli/commands/agents/suggestion/prompts/handoff.md +7 -0
  34. vidbyte_cli/commands/agents/suggestion/prompts/horizon.md +5 -0
  35. vidbyte_cli/commands/agents/suggestion/prompts/hypotheses.md +100 -0
  36. vidbyte_cli/commands/agents/suggestion/prompts/idea.md +100 -0
  37. vidbyte_cli/commands/agents/suggestion/prompts/in_progress.md +100 -0
  38. vidbyte_cli/commands/agents/suggestion/prompts/input.md +100 -0
  39. vidbyte_cli/commands/agents/suggestion/prompts/library.py +29 -0
  40. vidbyte_cli/commands/agents/suggestion/prompts/max_messages.md +5 -0
  41. vidbyte_cli/commands/agents/suggestion/prompts/max_output_tokens.md +5 -0
  42. vidbyte_cli/commands/agents/suggestion/prompts/max_total_tokens.md +5 -0
  43. vidbyte_cli/commands/agents/suggestion/prompts/mistakes.md +100 -0
  44. vidbyte_cli/commands/agents/suggestion/prompts/outcomes.md +100 -0
  45. vidbyte_cli/commands/agents/suggestion/prompts/project.md +100 -0
  46. vidbyte_cli/commands/agents/suggestion/prompts/project_create.md +7 -0
  47. vidbyte_cli/commands/agents/suggestion/prompts/project_description.md +98 -0
  48. vidbyte_cli/commands/agents/suggestion/prompts/project_group.md +7 -0
  49. vidbyte_cli/commands/agents/suggestion/prompts/project_key.md +96 -0
  50. vidbyte_cli/commands/agents/suggestion/prompts/project_list.md +7 -0
  51. vidbyte_cli/commands/agents/suggestion/prompts/project_title.md +99 -0
  52. vidbyte_cli/commands/agents/suggestion/prompts/provider.md +5 -0
  53. vidbyte_cli/commands/agents/suggestion/prompts/question.md +100 -0
  54. vidbyte_cli/commands/agents/suggestion/prompts/risks.md +100 -0
  55. vidbyte_cli/commands/agents/suggestion/prompts/rounds.md +5 -0
  56. vidbyte_cli/commands/agents/suggestion/prompts/run.md +5 -0
  57. vidbyte_cli/commands/agents/suggestion/prompts/success.md +100 -0
  58. vidbyte_cli/commands/agents/suggestion/prompts/timeout.md +5 -0
  59. vidbyte_cli/commands/agents/suggestion/prompts/trajectory.md +100 -0
  60. vidbyte_cli/commands/agents/suggestion/prompts/view.md +5 -0
  61. vidbyte_cli/commands/agents/suggestion/prompts/view_all.md +5 -0
  62. vidbyte_cli/commands/agents/suggestion/render.py +77 -0
  63. vidbyte_cli/commands/agents/suggestion/request_builder.py +240 -0
  64. vidbyte_cli/commands/agents/suggestion/suggest.py +154 -0
  65. vidbyte_cli/commands/agents/suggestion/suggestion_categories.py +77 -0
  66. vidbyte_cli/commands/agents/suggestion/suggestion_feedback.py +85 -0
  67. vidbyte_cli/commands/agents/suggestion/suggestion_handoff.py +67 -0
  68. vidbyte_cli/commands/agents/suggestion/suggestion_projects.py +95 -0
  69. vidbyte_cli/commands/auth/__init__.py +0 -0
  70. vidbyte_cli/commands/auth/login.py +100 -0
  71. vidbyte_cli/commands/auth/logout.py +41 -0
  72. vidbyte_cli/commands/auth/whoami.py +51 -0
  73. vidbyte_cli/commands/billing/__init__.py +20 -0
  74. vidbyte_cli/commands/billing/prompts/__init__.py +1 -0
  75. vidbyte_cli/commands/billing/prompts/confirm.md +16 -0
  76. vidbyte_cli/commands/billing/prompts/group.md +13 -0
  77. vidbyte_cli/commands/billing/prompts/idempotency_key.md +16 -0
  78. vidbyte_cli/commands/billing/prompts/library.py +18 -0
  79. vidbyte_cli/commands/billing/prompts/top_up.md +16 -0
  80. vidbyte_cli/commands/billing/top_up.py +65 -0
  81. vidbyte_cli/commands/config/__init__.py +0 -0
  82. vidbyte_cli/commands/config/get.py +45 -0
  83. vidbyte_cli/commands/config/set.py +39 -0
  84. vidbyte_cli/commands/provider/__init__.py +9 -0
  85. vidbyte_cli/commands/provider/login.py +102 -0
  86. vidbyte_cli/commands/provider/logout.py +45 -0
  87. vidbyte_cli/commands/provider/whoami.py +49 -0
  88. vidbyte_cli/commands/research/__init__.py +5 -0
  89. vidbyte_cli/commands/research/add.py +41 -0
  90. vidbyte_cli/commands/research/options.py +134 -0
  91. vidbyte_cli/commands/research/render.py +157 -0
  92. vidbyte_cli/commands/research/resume.py +37 -0
  93. vidbyte_cli/commands/research/start.py +37 -0
  94. vidbyte_cli/commands/research/status.py +32 -0
  95. vidbyte_cli/commands/research/thread.py +30 -0
  96. vidbyte_cli/commands/research/threads.py +35 -0
  97. vidbyte_cli/commands/research/watch.py +93 -0
  98. vidbyte_cli/commands/runtime/__init__.py +23 -0
  99. vidbyte_cli/commands/runtime/adversarial_team.py +41 -0
  100. vidbyte_cli/commands/runtime/doctor.py +44 -0
  101. vidbyte_cli/commands/runtime/list.py +47 -0
  102. vidbyte_cli/commands/runtime/persistence.py +141 -0
  103. vidbyte_cli/commands/runtime/same_host_ensemble.py +221 -0
  104. vidbyte_cli/commands/runtime/stages.py +553 -0
  105. vidbyte_cli/commands/runtime/task_board.py +1342 -0
  106. vidbyte_cli/commands/setup/__init__.py +0 -0
  107. vidbyte_cli/commands/setup/doctor.py +64 -0
  108. vidbyte_cli/commands/setup/prompts/__init__.py +1 -0
  109. vidbyte_cli/commands/setup/prompts/browser.md +7 -0
  110. vidbyte_cli/commands/setup/prompts/library.py +18 -0
  111. vidbyte_cli/commands/setup/prompts/star.md +13 -0
  112. vidbyte_cli/commands/setup/star.py +132 -0
  113. vidbyte_cli/lib/__init__.py +0 -0
  114. vidbyte_cli/lib/api/__init__.py +0 -0
  115. vidbyte_cli/lib/api/client.py +236 -0
  116. vidbyte_cli/lib/api/endpoints/__init__.py +0 -0
  117. vidbyte_cli/lib/api/endpoints/auth.py +24 -0
  118. vidbyte_cli/lib/api/endpoints/billing.py +36 -0
  119. vidbyte_cli/lib/api/endpoints/research.py +103 -0
  120. vidbyte_cli/lib/api/endpoints/runtime.py +113 -0
  121. vidbyte_cli/lib/api/problem.py +86 -0
  122. vidbyte_cli/lib/api/response.py +101 -0
  123. vidbyte_cli/lib/api/retry.py +116 -0
  124. vidbyte_cli/lib/api/runtime_payment.py +89 -0
  125. vidbyte_cli/lib/auth/__init__.py +16 -0
  126. vidbyte_cli/lib/auth/credentials.py +223 -0
  127. vidbyte_cli/lib/auth/input.py +59 -0
  128. vidbyte_cli/lib/auth/keyring_store.py +126 -0
  129. vidbyte_cli/lib/auth/provider_credentials.py +157 -0
  130. vidbyte_cli/lib/auth/provider_input.py +54 -0
  131. vidbyte_cli/lib/auth/provider_resolver.py +89 -0
  132. vidbyte_cli/lib/auth/provider_store.py +149 -0
  133. vidbyte_cli/lib/auth/provider_verifier.py +219 -0
  134. vidbyte_cli/lib/auth/resolver.py +63 -0
  135. vidbyte_cli/lib/auth/verifier.py +38 -0
  136. vidbyte_cli/lib/config/__init__.py +25 -0
  137. vidbyte_cli/lib/config/atomic.py +47 -0
  138. vidbyte_cli/lib/config/config.py +141 -0
  139. vidbyte_cli/lib/config/migration.py +81 -0
  140. vidbyte_cli/lib/config/models.py +145 -0
  141. vidbyte_cli/lib/config/paths.py +66 -0
  142. vidbyte_cli/lib/config/resolver.py +137 -0
  143. vidbyte_cli/lib/constants/__init__.py +1 -0
  144. vidbyte_cli/lib/constants/runtime.py +186 -0
  145. vidbyte_cli/lib/errors/__init__.py +12 -0
  146. vidbyte_cli/lib/errors/cli_error.py +86 -0
  147. vidbyte_cli/lib/errors/codes.py +43 -0
  148. vidbyte_cli/lib/errors/failures.py +2910 -0
  149. vidbyte_cli/lib/errors/handler.py +72 -0
  150. vidbyte_cli/lib/files/__init__.py +6 -0
  151. vidbyte_cli/lib/files/documents.py +94 -0
  152. vidbyte_cli/lib/files/store.py +156 -0
  153. vidbyte_cli/lib/io/__init__.py +16 -0
  154. vidbyte_cli/lib/io/attachments.py +129 -0
  155. vidbyte_cli/lib/io/codex_attachments.py +47 -0
  156. vidbyte_cli/lib/io/prompt.py +71 -0
  157. vidbyte_cli/lib/io/streams.py +37 -0
  158. vidbyte_cli/lib/io/terminal.py +66 -0
  159. vidbyte_cli/lib/output/__init__.py +7 -0
  160. vidbyte_cli/lib/output/formats.py +29 -0
  161. vidbyte_cli/lib/output/manager.py +95 -0
  162. vidbyte_cli/lib/output/models.py +49 -0
  163. vidbyte_cli/lib/runtime/__init__.py +9 -0
  164. vidbyte_cli/lib/runtime/application.py +134 -0
  165. vidbyte_cli/lib/runtime/context.py +250 -0
  166. vidbyte_cli/lib/runtime/options.py +159 -0
  167. vidbyte_cli/lib/runtime/version.py +30 -0
  168. vidbyte_cli/lib/runtime_primitives/__init__.py +28 -0
  169. vidbyte_cli/lib/runtime_primitives/executor.py +54 -0
  170. vidbyte_cli/lib/runtime_primitives/gate.py +105 -0
  171. vidbyte_cli/lib/runtime_primitives/hosts.py +45 -0
  172. vidbyte_cli/lib/runtime_primitives/planner.py +79 -0
  173. vidbyte_cli/lib/runtime_primitives/stages.py +338 -0
  174. vidbyte_cli/lib/runtime_primitives/task_board.py +982 -0
  175. vidbyte_cli/lib/runtime_primitives/task_board_checkpoints.py +473 -0
  176. vidbyte_cli/lib/runtime_primitives/task_board_system.md +33 -0
  177. vidbyte_cli/lib/runtime_primitives/task_board_tasks.py +111 -0
  178. vidbyte_cli/lib/runtime_primitives/verification.py +68 -0
  179. vidbyte_cli/services/__init__.py +5 -0
  180. vidbyte_cli/services/ensemble/__init__.py +7 -0
  181. vidbyte_cli/services/ensemble/prompts/__init__.py +6 -0
  182. vidbyte_cli/services/ensemble/prompts/implementer_system.md +546 -0
  183. vidbyte_cli/services/ensemble/prompts/implementer_turn.md +49 -0
  184. vidbyte_cli/services/ensemble/prompts/library.py +188 -0
  185. vidbyte_cli/services/ensemble/prompts/planner_system.md +728 -0
  186. vidbyte_cli/services/ensemble/prompts/planner_turn.md +6 -0
  187. vidbyte_cli/services/ensemble/prompts/role_system.md +104 -0
  188. vidbyte_cli/services/ensemble/prompts/role_turn.md +19 -0
  189. vidbyte_cli/services/ensemble/prompts/selector_final_turn.md +19 -0
  190. vidbyte_cli/services/ensemble/prompts/selector_round_turn.md +17 -0
  191. vidbyte_cli/services/ensemble/prompts/selector_system.md +89 -0
  192. vidbyte_cli/services/ensemble/runner.py +30 -0
  193. vidbyte_cli/services/ensemble/sdk.py +134 -0
  194. vidbyte_cli/services/ensemble/service.py +233 -0
  195. vidbyte_cli/services/ensemble/settings.py +60 -0
  196. vidbyte_cli/services/persistence/__init__.py +6 -0
  197. vidbyte_cli/services/persistence/prompts/__init__.py +6 -0
  198. vidbyte_cli/services/persistence/prompts/library.py +46 -0
  199. vidbyte_cli/services/persistence/prompts/persistence_system.md +1 -0
  200. vidbyte_cli/services/persistence/prompts/persistence_turn.md +3 -0
  201. vidbyte_cli/services/persistence/runner.py +35 -0
  202. vidbyte_cli/services/persistence/session.py +116 -0
  203. vidbyte_cli/services/suggestions/__init__.py +7 -0
  204. vidbyte_cli/services/suggestions/categories.py +270 -0
  205. vidbyte_cli/services/suggestions/context.py +222 -0
  206. vidbyte_cli/services/suggestions/extra_compute.py +53 -0
  207. vidbyte_cli/services/suggestions/handoff.py +173 -0
  208. vidbyte_cli/services/suggestions/message_tools.py +47 -0
  209. vidbyte_cli/services/suggestions/project.py +253 -0
  210. vidbyte_cli/services/suggestions/prompts/__init__.py +1 -0
  211. vidbyte_cli/services/suggestions/prompts/categories/__init__.py +3 -0
  212. vidbyte_cli/services/suggestions/prompts/categories/adjacent_opportunity.md +37 -0
  213. vidbyte_cli/services/suggestions/prompts/categories/alternative.md +38 -0
  214. vidbyte_cli/services/suggestions/prompts/categories/big_bets.md +37 -0
  215. vidbyte_cli/services/suggestions/prompts/categories/bottleneck.md +38 -0
  216. vidbyte_cli/services/suggestions/prompts/categories/business_growth.md +38 -0
  217. vidbyte_cli/services/suggestions/prompts/categories/completion.md +37 -0
  218. vidbyte_cli/services/suggestions/prompts/categories/continuation.md +37 -0
  219. vidbyte_cli/services/suggestions/prompts/categories/coordination.md +37 -0
  220. vidbyte_cli/services/suggestions/prompts/categories/creative_exploration.md +37 -0
  221. vidbyte_cli/services/suggestions/prompts/categories/cross_domain.md +37 -0
  222. vidbyte_cli/services/suggestions/prompts/categories/delegation.md +37 -0
  223. vidbyte_cli/services/suggestions/prompts/categories/experiment.md +37 -0
  224. vidbyte_cli/services/suggestions/prompts/categories/feedback.md +37 -0
  225. vidbyte_cli/services/suggestions/prompts/categories/goal_clarification.md +37 -0
  226. vidbyte_cli/services/suggestions/prompts/categories/immediate_next_steps.md +37 -0
  227. vidbyte_cli/services/suggestions/prompts/categories/investigation.md +37 -0
  228. vidbyte_cli/services/suggestions/prompts/categories/learning.md +37 -0
  229. vidbyte_cli/services/suggestions/prompts/categories/leverage.md +37 -0
  230. vidbyte_cli/services/suggestions/prompts/categories/long_term_directions.md +37 -0
  231. vidbyte_cli/services/suggestions/prompts/categories/preparation.md +37 -0
  232. vidbyte_cli/services/suggestions/prompts/categories/prerequisite.md +37 -0
  233. vidbyte_cli/services/suggestions/prompts/categories/prioritization.md +37 -0
  234. vidbyte_cli/services/suggestions/prompts/categories/product_experience.md +37 -0
  235. vidbyte_cli/services/suggestions/prompts/categories/quick_wins.md +37 -0
  236. vidbyte_cli/services/suggestions/prompts/categories/reframing.md +37 -0
  237. vidbyte_cli/services/suggestions/prompts/categories/risk_prevention.md +37 -0
  238. vidbyte_cli/services/suggestions/prompts/categories/simplification.md +37 -0
  239. vidbyte_cli/services/suggestions/prompts/categories/stop_or_defer.md +37 -0
  240. vidbyte_cli/services/suggestions/prompts/categories/strategy.md +37 -0
  241. vidbyte_cli/services/suggestions/prompts/categories/technical_possibilities.md +37 -0
  242. vidbyte_cli/services/suggestions/prompts/categories/verification.md +37 -0
  243. vidbyte_cli/services/suggestions/prompts/critic.md +209 -0
  244. vidbyte_cli/services/suggestions/prompts/critic_message.md +8 -0
  245. vidbyte_cli/services/suggestions/prompts/generator.md +64 -0
  246. vidbyte_cli/services/suggestions/prompts/library.py +72 -0
  247. vidbyte_cli/services/suggestions/prompts/message_generator_argument.md +1 -0
  248. vidbyte_cli/services/suggestions/prompts/message_generator_tool.md +1 -0
  249. vidbyte_cli/services/suggestions/prompts/message_parent_argument.md +1 -0
  250. vidbyte_cli/services/suggestions/prompts/message_parent_tool.md +1 -0
  251. vidbyte_cli/services/suggestions/prompts/message_receipt.md +1 -0
  252. vidbyte_cli/services/suggestions/prompts/revision.md +8 -0
  253. vidbyte_cli/services/suggestions/sdk.py +228 -0
  254. vidbyte_cli/services/suggestions/selection.py +78 -0
  255. vidbyte_cli/services/suggestions/service.py +540 -0
  256. vidbyte_cli/types/__init__.py +0 -0
  257. vidbyte_cli/types/api.py +101 -0
  258. vidbyte_cli/types/attachments.py +79 -0
  259. vidbyte_cli/types/ensemble.py +189 -0
  260. vidbyte_cli/types/provider.py +62 -0
  261. vidbyte_cli/types/research.py +260 -0
  262. vidbyte_cli/types/runtime.py +1098 -0
  263. vidbyte_cli/types/suggestions.py +518 -0
  264. vidbyte_cli-0.1.0.dist-info/METADATA +335 -0
  265. vidbyte_cli-0.1.0.dist-info/RECORD +269 -0
  266. vidbyte_cli-0.1.0.dist-info/WHEEL +5 -0
  267. vidbyte_cli-0.1.0.dist-info/entry_points.txt +2 -0
  268. vidbyte_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  269. vidbyte_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1342 @@
1
+ """The task-board command group: running a board, and reading the boards already stored.
2
+
3
+ `run` validates locally, buys one admission, verifies the grant, then executes offline. Every
4
+ other verb here is free and offline: `list`, `status`, `show-step`, and `export-tasks` only
5
+ read a checkpoint directory, and `fork` only copies one. A board is built from literal task
6
+ arguments, whole Markdown files, one task-list file, or the task list a stored board recorded.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from collections.abc import Callable, Mapping
13
+ from pathlib import Path
14
+ from uuid import uuid4
15
+
16
+ import click
17
+
18
+ from ...lib.constants.runtime import TaskBoardLimit
19
+ from ...lib.constants.runtime import TaskBoardProgress as Progress
20
+ from ...lib.errors.failures import (
21
+ LocalFileReadFailed,
22
+ LocalFileWriteFailed,
23
+ RuntimeAdmissionNotVerified,
24
+ TaskBoardBoardIdInvalid,
25
+ TaskBoardCheckpointMissing,
26
+ TaskBoardCheckpointRootUnreadable,
27
+ TaskBoardDecomposeInvalid,
28
+ TaskBoardDependencyInvalid,
29
+ TaskBoardForkFailed,
30
+ TaskBoardForkTargetExists,
31
+ TaskBoardInputInvalid,
32
+ TaskBoardOutputWriteFailed,
33
+ TaskBoardStepNotStored,
34
+ )
35
+ from ...lib.files import LocalFileStore
36
+ from ...lib.output import OutputDocument
37
+ from ...lib.runtime.context import ApplicationContext as Context
38
+ from ...lib.runtime_primitives.gate import RuntimeAdmissionGate
39
+ from ...lib.runtime_primitives.task_board import TaskBoardCodexSession
40
+ from ...lib.runtime_primitives.task_board_checkpoints import TaskBoardCheckpointer
41
+ from ...lib.runtime_primitives.task_board_tasks import TaskBoardTaskFiles
42
+ from ...types.provider import PROVIDER_ENV_VARS, Provider
43
+ from ...types.runtime import (
44
+ RuntimeAdmissionRequest,
45
+ RuntimeGrantVerificationRequest,
46
+ RuntimeHost,
47
+ TaskBoardAgentSettings,
48
+ TaskBoardCheckpoint,
49
+ TaskBoardCheckpointChain,
50
+ TaskBoardCheckpointMode,
51
+ TaskBoardContextMode,
52
+ TaskBoardExecutionType,
53
+ TaskBoardForkResult,
54
+ TaskBoardHandoffMode,
55
+ TaskBoardListing,
56
+ TaskBoardPromptPreview,
57
+ TaskBoardReasoningEffort,
58
+ TaskBoardRunControls,
59
+ TaskBoardSandbox,
60
+ TaskBoardSettings,
61
+ TaskBoardStatusReport,
62
+ TaskBoardStepDetail,
63
+ TaskBoardSummaryMode,
64
+ TaskBoardTaskExport,
65
+ )
66
+
67
+ _DEFAULT_CHECKPOINT_ROOT = Path(".vidbyte") / "task-board"
68
+
69
+ _GROUP_HELP = (
70
+ "Run an ordered board of tasks, each in its own Codex agent, and inspect the boards this "
71
+ "machine has already stored. The run subcommand is the only one that costs anything: it "
72
+ "buys one two-cent Vidbyte admission for the whole board and bills every model call to "
73
+ "your own OpenAI account. Every other subcommand here reads or copies a checkpoint "
74
+ "directory offline and for free, which is what makes it safe for an agent to call list, "
75
+ "status, or show-step as often as it likes while deciding what to do next. Boards are "
76
+ "addressed by a checkpoint root plus a board id, so several boards can exist side by side."
77
+ )
78
+ _COMMAND_HELP = (
79
+ "Run an ordered board of tasks, each in its own Codex agent, on this machine. Give the "
80
+ "board either as literal TASKS arguments, where every argument is one complete task "
81
+ "statement in board order, as repeated --task-file paths, where each whole Markdown file "
82
+ "is one task, or as a single --task-list file holding the whole board; these forms cannot "
83
+ "be mixed because that would leave board order ambiguous, and a resume may instead reuse "
84
+ "the task list its stored board recorded. Tasks always run strictly in the order given, "
85
+ "one at a time, and each agent is thrown away when its task ends so no thread is ever "
86
+ "reused. By default an agent also reads bounded summaries of the few tasks immediately "
87
+ "before it, which you can narrow with --window or switch off entirely with --context-mode "
88
+ "isolated. Pass --type dag with repeatable --depends-on links when tasks have explicit "
89
+ "dependencies, so each agent reads only its own dependencies and runs after them instead "
90
+ "of after every earlier task. Vidbyte charges two cents to admit the whole run and every "
91
+ "model call is billed to your own OpenAI account. Pass --allow-decompose, together with "
92
+ "--no-checkpoint, when a task may be too broad for one agent: that task's agent can then "
93
+ "call a native decompose_tool to replace itself with two to --max-subtasks ordered child "
94
+ "tasks, which run in its place before the rest of the board, each in a fresh isolated "
95
+ "agent that cannot split again."
96
+ )
97
+ _TYPE_HELP = (
98
+ "Which loop structure the board run follows. In linear mode, the default, tasks run "
99
+ "strictly in the order given and each agent reads bounded summaries of the immediately "
100
+ "preceding --window tasks, which preserves the original board behavior exactly. In dag "
101
+ "mode tasks run once each in deterministic topological order and each agent reads only "
102
+ "its direct dependencies' summaries, so a task that needs only task 2 never pays for "
103
+ "tasks 0, 1, or 3 through 7. Execution stays sequential in both modes with one fresh "
104
+ "agent per task, and dag mode ignores --window when selecting context while keeping "
105
+ "--handoff, --summary-mode, and --summary-max-chars for each dependency entry. The type "
106
+ "and links are stored with the board, so a resume must repeat them."
107
+ )
108
+ _DEPENDS_ON_HELP = (
109
+ "One dependency link of the form CHILD:PARENT[,PARENT...], using 0-based board positions "
110
+ "where the board order is the order of TASKS arguments, --task-file paths, or --task-list "
111
+ "entries. Repeat the option once per link, so --depends-on 8:2 --depends-on 5:2,3 means "
112
+ "task 8 reads task 2 and task 5 reads tasks 2 and 3; repeating a child across flags "
113
+ "accumulates its parents. Every child and parent must sit inside the board, self-links and "
114
+ "duplicate links are rejected, and the full link set must be acyclic. Links require --type "
115
+ "dag and are rejected with --type linear, and every link is validated before credentials, "
116
+ "payment, or host execution."
117
+ )
118
+ _ALLOW_DECOMPOSE_HELP = (
119
+ "Whether a task's agent may call the native decompose_tool to replace that task with an "
120
+ "ordered list of self-contained subtasks, which then run in its place, one at a time, "
121
+ "before any later task on the board. The default is off, because a split changes the "
122
+ "board's length while it runs and so gives up checkpoints, dependency links, and every "
123
+ "prior-results window, none of which an ordinary board should lose. Turn it on when some "
124
+ "tasks may be too broad for one agent and you would rather let that agent plan the split "
125
+ "than write the finer-grained board yourself. While it is on, every parent and every "
126
+ "child runs with --context-mode isolated regardless of what you pass, each child sees "
127
+ "only its own subtask string, children can never decompose again, and a parent that "
128
+ "declines the tool simply completes its task as usual. The parent's turn is billed to "
129
+ "your OpenAI account whether or not it splits, and each child is another billed turn, so "
130
+ "a board can cost several times more model usage than its task count suggests while the "
131
+ "two-cent Vidbyte admission stays the same. It requires --no-checkpoint and --type "
132
+ "linear, and either conflict is rejected before credentials, payment, or any agent starts."
133
+ )
134
+ _MAX_SUBTASKS_HELP = (
135
+ "The largest number of child tasks one parent may create in a single decompose_tool "
136
+ "call, from 2 to 10, and the value is ignored unless --allow-decompose is also passed. "
137
+ "The default of 5 is enough to separate the distinct pieces of a broad task without "
138
+ "letting one parent fan out into many small turns that each start a fresh agent and each "
139
+ "bill your OpenAI account. Lower it when tasks are already close to the right size and "
140
+ "you only want the occasional two- or three-way split; raise it when individual tasks "
141
+ "routinely bundle many independent deliverables. The model's candidate list is stripped, "
142
+ "entries that are blank or longer than 20,000 characters are dropped, and "
143
+ "case-insensitive duplicates keep only their first occurrence before this cap truncates "
144
+ "the rest, and a call left with fewer than two valid children is rejected so the task "
145
+ "stays whole. The board as a whole also stops at 500 live tasks, so a parent near that "
146
+ "ceiling gets a smaller effective cap, and a parent with room for fewer than two "
147
+ "children is never offered the tool at all."
148
+ )
149
+ _LIST_HELP = (
150
+ "List every checkpointed board stored under one checkpoint root, one line each. Each row "
151
+ "reports the board id, its task count, how many steps completed, failed, and remain, and "
152
+ "the absolute directory the board lives in. Reading a board runs no model turns and costs "
153
+ "nothing, so an agent holding ten boards can call this freely to find the unfinished one. "
154
+ "Use --checkpoint-root when the boards were saved somewhere other than the default "
155
+ "directory, and follow up with status on whichever board id you want details for."
156
+ )
157
+ _STATUS_HELP = (
158
+ "Report one stored board's progress without running or paying for anything. The result "
159
+ "names which step indices completed, which failed, and which are still pending, along with "
160
+ "the reported token total, the estimated spend, and the absolute board directory. It also "
161
+ "returns a ready-to-paste resume command, so the decision to continue, repair, fork, or "
162
+ "abandon can be made from facts rather than guesses. Pass --report-file to additionally "
163
+ "write the same history out as a Markdown document a person can read."
164
+ )
165
+ _SHOW_STEP_HELP = (
166
+ "Return everything stored for one board step in a single response. The record holds the "
167
+ "absolute path of its checkpoint file, the full prompt the step received, its bounded "
168
+ "summary, its status, its thread id, and its reported token count. Because all of it "
169
+ "arrives together, debugging one step costs one call instead of several raw file reads. "
170
+ "The step must already exist on disk, so run status first if you are unsure which indices "
171
+ "this board has stored."
172
+ )
173
+ _FORK_HELP = (
174
+ "Copy a stored board's first --at steps into a new board id and leave the original "
175
+ "untouched. Forking is how you try two different endings from the same prefix: the copy "
176
+ "gets its own directory, its own manifest, and a recorded edge back to the board it came "
177
+ "from, so neither history can overwrite the other. Nothing runs and nothing is charged; "
178
+ "the fork only duplicates checkpoint files that already exist. The result returns the new "
179
+ "board's absolute directory and the resume command that continues it."
180
+ )
181
+ _EXPORT_TASKS_HELP = (
182
+ "Write a stored board's task list back out as a reviewable file. Pass a .md destination to "
183
+ "get one level-two section per task, or a .json destination to get a flat array of task "
184
+ "strings; both round-trip exactly through --task-list. Exporting is how a board that "
185
+ "started as shell arguments becomes a file you can commit, review, and re-run later. "
186
+ "Nothing runs and nothing is charged, because this only reads the board manifest."
187
+ )
188
+ _TASK_FILE_HELP = (
189
+ "Path to one Markdown file whose entire contents are a single board task. Repeat the "
190
+ "option once per task, in the order the tasks should run, when a task is too long or too "
191
+ "structured to pass as a shell argument. The file is read whole and used verbatim, so its "
192
+ "headings, lists, and blank lines all reach the agent exactly as written, and its own "
193
+ "lines are never split into separate tasks. Each path must end in .md and hold something "
194
+ "other than whitespace, and this option cannot be combined with any other board source."
195
+ )
196
+ _TASK_LIST_HELP = (
197
+ "Path to one file holding the whole board, so a hundred-task board is reviewable in git "
198
+ "instead of pasted into a shell. A .md file is split on its level-two headings, one task "
199
+ "per section with the heading line dropped, and a .json file is read as a flat array of "
200
+ "task strings. Board order is exactly the order the file lists, and nothing is merged, "
201
+ "reordered, or split further. This option cannot be combined with literal TASKS arguments "
202
+ "or --task-file, and `export-tasks` writes the same two shapes back out."
203
+ )
204
+ _HOST_HELP = (
205
+ "Which installed native coding agent runs every task on this board. Only codex is "
206
+ "supported in v1, so this option exists to make the host explicit in scripts and to keep "
207
+ "the flag stable as more hosts are admitted later. Passing auto asks the CLI to resolve "
208
+ "whichever supported host it can find on PATH, which today can only ever resolve to "
209
+ "codex. The backend refuses admission for any other host, so an unavailable or "
210
+ "unsupported host fails before payment rather than part-way through the board."
211
+ )
212
+ _WINDOW_HELP = (
213
+ "How many immediately preceding task results an agent may read, counted backwards from "
214
+ "the task about to run. With the default of 10, the agent for task 90 receives summaries "
215
+ "of tasks 80 through 89 and nothing earlier, which is what keeps prompt size flat however "
216
+ "long the board grows. A window of 0 leaves an agent with only its own task while still "
217
+ "telling it that earlier tasks ran, and a window larger than the number of finished tasks "
218
+ "simply clamps to what exists. This option is ignored when --context-mode is isolated."
219
+ )
220
+ _CONTEXT_MODE_HELP = (
221
+ "Whether an agent is told anything at all about the tasks that ran before it. In "
222
+ "windowed-summaries mode, the default, each agent reads bounded summaries of the previous "
223
+ "--window results, which is what you want when later tasks build on earlier ones. In "
224
+ "isolated mode no prior result reaches any agent and the prompt carries no prior-results "
225
+ "section whatsoever, which is the correct choice for a board of independently decomposed "
226
+ "tasks that must not inherit each other's assumptions. Isolated mode overrides --window, "
227
+ "--handoff, --summary-mode, and --summary-max-chars instead of combining with them."
228
+ )
229
+ _HANDOFF_HELP = (
230
+ "What each finished task actually hands to the later tasks allowed to read it. In summary "
231
+ "mode, the default, a reader sees only the bounded summary of each prior result, which is "
232
+ "what keeps prompts flat on a long board. In task-and-summary mode every entry is prefixed "
233
+ "with the prior task's own statement, so a reader learns what was asked as well as what "
234
+ "came back, which helps when tasks are terse. In full-result mode the untruncated result "
235
+ "text is forwarded instead, bounded only by the 20,000-character task ceiling, which is "
236
+ "accurate but grows prompts fast and suits only short boards. It does nothing in isolated "
237
+ "mode, and changing it invalidates a stored board for resume because prompts would differ."
238
+ )
239
+ _SUMMARY_MODE_HELP = (
240
+ "The shape of each prior-result summary once that result is longer than "
241
+ "--summary-max-chars. truncate-tail keeps the opening of the result and names how many "
242
+ "characters were dropped, which favors a task's setup and reasoning. head-tail splits the "
243
+ "budget evenly between the opening and the closing of the result, which is what preserves "
244
+ "a concluding answer or a final file listing. Neither mode calls a model, so "
245
+ "summarization stays deterministic and costs nothing, and both are ignored when "
246
+ "--context-mode is isolated."
247
+ )
248
+ _SUMMARY_MAX_CHARS_HELP = (
249
+ "The character budget for one prior-result summary before --summary-mode starts dropping "
250
+ "content. A result at or under this length is passed forward untouched, and anything "
251
+ "longer is shortened and marked with the exact number of characters removed. Multiply "
252
+ "this budget by --window to predict the worst-case size of the prior-results block a "
253
+ "single agent will read. Accepted values run from 100 to 8000 characters, and the setting "
254
+ "is ignored when --context-mode is isolated."
255
+ )
256
+ _STOP_ON_ERROR_HELP = (
257
+ "What the board does the first time a task exhausts its retries. With --stop-on-error, "
258
+ "the default, the board halts immediately, keeps every result completed before the "
259
+ "failure, and returns that prefix rather than running work that depends on a task which "
260
+ "never finished. With --no-stop-on-error the failed task is recorded with a failed "
261
+ "status, a short placeholder stands in for its summary so board indices stay aligned, and "
262
+ "the next task starts anyway. Keep the default for a board whose tasks depend on each "
263
+ "other, and turn it off for independent tasks where one failure should not cancel the rest."
264
+ )
265
+ _RETRIES_PER_TASK_HELP = (
266
+ "How many extra attempts one task receives after its first attempt fails, before the "
267
+ "board treats that task as failed. Every retry sends the identical prompt to a brand-new "
268
+ "agent, so a retry recovers from a crashed or timed-out host rather than from a task the "
269
+ "agent understood but could not do. Each attempt is billed to your own OpenAI account, so "
270
+ "a high retry count on a large board multiplies model cost even though the Vidbyte "
271
+ "admission is charged once. Accepted values run from 0 to 3, and a task that succeeds on "
272
+ "a retry produces exactly one result entry, never a duplicate."
273
+ )
274
+ _MODEL_HELP = (
275
+ "Which Codex model every task agent on this board runs, given as the provider's own model "
276
+ "identifier. Leaving it unset keeps whatever model the installed Codex host defaults to, "
277
+ "which is the right choice when the board is not model-sensitive. Set it when a board "
278
+ "needs a specific capability or price point, because one value applies to every iteration "
279
+ "and no individual task can override it. The model is charged to your own OpenAI account "
280
+ "rather than to the flat Vidbyte admission."
281
+ )
282
+ _SANDBOX_HELP = (
283
+ "How much of the filesystem every task agent on this board may change. workspace-write, "
284
+ "the default, lets a task edit files inside the working directory and is what a board of "
285
+ "implementation tasks needs. read-only lets a task inspect the tree without writing to "
286
+ "it, which is the correct setting for a board of review, audit, or planning tasks. "
287
+ "full-access removes the sandbox entirely and should be reserved for boards whose tasks "
288
+ "genuinely have to reach outside the workspace. One setting applies to the whole board."
289
+ )
290
+ _REASONING_EFFORT_HELP = (
291
+ "How much reasoning each task turn asks the model to spend before answering. Leaving it "
292
+ "unset keeps the provider default, which is what most boards want. Raising it to high or "
293
+ "xhigh suits a board of a few genuinely hard tasks, while minimal or low suits a long "
294
+ "board of mechanical ones. Effort multiplies your own provider bill on every task, so a "
295
+ "500-task board at xhigh is a real cost decision rather than a tuning knob."
296
+ )
297
+ _TURN_TIMEOUT_HELP = (
298
+ "How long one task attempt may run before the board cancels it and counts that attempt as "
299
+ "failed. The default of five days is a ceiling on a wedged child process rather than a "
300
+ "budget any task is expected to approach, because one board task can legitimately hold a "
301
+ "coding agent for a long time. Lower it when a board's tasks are small and a stuck agent "
302
+ "should surface quickly instead of blocking the whole run. The timeout applies per "
303
+ "attempt, so each retry gets the full allowance again."
304
+ )
305
+ _IDEMPOTENCY_KEY_HELP = (
306
+ "A caller-supplied key that makes the paid admission for this run replay-safe. Leave it "
307
+ "unset and the CLI generates a fresh key, which is correct for every new board. Reuse the "
308
+ "key from an earlier invocation only to recover an admission whose response you never "
309
+ "saw, so the wallet is not charged a second time for the same run. Reusing a key does not "
310
+ "resume or deduplicate the board itself: use --from for that, and every model call is "
311
+ "billed to your own OpenAI account again."
312
+ )
313
+ _CHECKPOINT_HELP = (
314
+ "Whether every finished step is saved under the checkpoint root as it completes, so a "
315
+ "later invocation can resume, repair, replay, or fork without re-running paid turns. Each "
316
+ "step stores its prompt, bounded summary, thread ID, and reported token count in one "
317
+ "atomic JSON file, plus a manifest holding the board and its context settings. "
318
+ "Checkpointing is on by default and costs nothing beyond local disk. Pass --no-checkpoint "
319
+ "for a run that must leave no trace, noting that every resume and reporting option then "
320
+ "has nothing to read and is rejected."
321
+ )
322
+ _CHECKPOINT_ROOT_HELP = (
323
+ "The directory that holds boards, one subdirectory per board id. It defaults to "
324
+ ".vidbyte/task-board under the current working directory, which keeps a project's boards "
325
+ "with the project. Point it somewhere explicit, such as ./runs/exp-a, to isolate one "
326
+ "experiment so several boards can run side by side without ever colliding. The path is "
327
+ "resolved to an absolute directory and returned in the result, so a later command can "
328
+ "address the board from any working directory. It is created on the first checkpoint write."
329
+ )
330
+ _CHECKPOINT_ID_HELP = (
331
+ "The directory name identifying one board inside the checkpoint root. Leave it unset on a "
332
+ "run and the CLI derives twelve hex characters from a hash of the task list, so re-running "
333
+ "the identical board naturally finds its own prior steps. Set it explicitly to keep "
334
+ "several runs of an evolving board apart, or to address a specific stored board when "
335
+ "resuming, forking, or inspecting one. The value holds 1 to 64 letters, digits, dots, "
336
+ "underscores, or hyphens, and on a run it requires checkpointing to stay enabled."
337
+ )
338
+ _CHECKPOINT_MODE_HELP = (
339
+ "What happens, beyond the durable save, each time a step is checkpointed. save-only, the "
340
+ "default, writes the step file silently and is what an unattended overnight board wants. "
341
+ "stream additionally emits one record per step on the results stream, so a parent agent "
342
+ "watching with --format jsonl sees each summary as it lands instead of waiting for the "
343
+ "board to end. export additionally appends one line per step to a single append-only "
344
+ "file, which is what an outside forking or indexing tool should tail. The mode changes "
345
+ "visibility only: the same step files are written in all three."
346
+ )
347
+ _EXPORT_FILE_HELP = (
348
+ "Where --checkpoint-mode export appends its one-line-per-step log. Leave it unset and the "
349
+ "log is progress.jsonl inside the board directory, which keeps it with the board it "
350
+ "describes. Point it somewhere shared when several boards should feed one downstream "
351
+ "index. Because the file grows in completion order and is only ever appended to, a "
352
+ "forking tool can read the prefix it needs without scanning or sorting a directory. It is "
353
+ "ignored unless --checkpoint-mode is export."
354
+ )
355
+ _REPORT_FILE_HELP = (
356
+ "Write a Markdown report of the board's stored history to this path when the run ends. "
357
+ "The report opens with the totals — tasks, completed, failed, pending, tokens, estimated "
358
+ "spend — and then gives one section per stored step with its task and summary. Because "
359
+ "the format is stable prose rather than JSON, it can be attached to a ticket or handed to "
360
+ "someone who will never open a checkpoint file. It is written from the same stored chain "
361
+ "that status reads, so the document and the machine record can never disagree."
362
+ )
363
+ _ON_CHECKPOINT_HELP = (
364
+ "A shell command to run after each successful checkpoint save, with BOARD_DIR, BOARD_ID, "
365
+ "STEP_FILE, STEP_INDEX, and STEP_STATUS set in its environment. Use it to trigger an "
366
+ "embedding job, a backup, or a notification without waiting for these to become CLI "
367
+ "features. The hook is an observer and never a gate: it runs with a sixty-second timeout, "
368
+ "and a failure is reported on stderr while the board keeps its progress and continues. It "
369
+ "runs your own shell, so treat it exactly as seriously as any other command you would type."
370
+ )
371
+ _FROM_HELP = (
372
+ "Resume a previously checkpointed board starting at step INDEX, skipping paid turns for "
373
+ "every earlier step. Steps before INDEX load from disk and seed the handoff window exactly "
374
+ "as if they had just run, so step 70 sees the entries for steps 60 through 69 from "
375
+ "storage. Only INDEX through the end of the board execute, and nothing before it is "
376
+ "rewritten. If no task source is given, the board's own stored task list is reused, which "
377
+ "is what makes the returned resume command paste-able. Resuming still buys one fresh flat "
378
+ "admission, and the stored board and settings must match or the run is rejected before pay. "
379
+ "On a --type dag board INDEX counts steps in the topological order the board runs in, not "
380
+ "board positions, which is why the resume command a run returns is the one to paste."
381
+ )
382
+ _REPLAY_TASK_HELP = (
383
+ "Re-run exactly one board step for debugging, rebuilding its prompt byte-identically from "
384
+ "the task text plus the stored entries its window, or on a dag board its dependencies, "
385
+ "would have held. Every step that runs before it must be stored. Only the addressed step "
386
+ "executes, in a fresh agent with the normal retry policy, and only its checkpoint file is "
387
+ "overwritten. The rest of the board is untouched, which is what makes replay safe to "
388
+ "repeat while chasing a flaky step. It requires checkpoints from a prior run of the same "
389
+ "board and cannot be combined with --from."
390
+ )
391
+ _PRINT_PROMPT_HELP = (
392
+ "Rebuild the prompt --replay-task would send and return it without starting any agent. No "
393
+ "admission is bought, no model turn runs, and nothing on disk changes, so previewing is "
394
+ "free and instant. Use it to confirm the window really holds the summaries you expect "
395
+ "before spending on a turn that would only prove it did not. It requires --replay-task and "
396
+ "an existing checkpointed board, and it is the only run invocation that never pays."
397
+ )
398
+ _STOP_AFTER_HELP = (
399
+ "Stop this invocation after N newly executed steps and return control to the caller. The "
400
+ "board is not abandoned: every finished step is checkpointed as usual and the result "
401
+ "carries the resume command that continues from the next index. Use it to run a long board "
402
+ "in reviewable slices, so a parent agent can inspect ten steps, adjust, and then run the "
403
+ "next ten. Leaving it unset runs to the end of the board, which is the existing behavior."
404
+ )
405
+ _RETRY_FAILED_ONLY_HELP = (
406
+ "Re-run only the steps a previous run recorded as failed, instead of everything after the "
407
+ "resume point. A board with 97 successes and 3 failures then costs three model turns "
408
+ "rather than paying again for work that already landed. Successful steps keep their stored "
409
+ "results and still seed the handoff window, so a repaired step sees the same context it "
410
+ "would have seen originally. It requires an existing checkpointed board, and --from acts "
411
+ "as a floor on which failures are eligible."
412
+ )
413
+ _MAX_TOKENS_HELP = (
414
+ "Halt the board once the reported provider tokens for this invocation reach this many. "
415
+ "The check happens between steps, so at most one step can overshoot the limit rather than "
416
+ "a runaway board burning through a budget unnoticed. Steps whose provider reported no "
417
+ "usage count as zero here, because an unknown token count is never guessed at. When the "
418
+ "limit stops a board the result says so and carries the resume command, so the run can "
419
+ "continue after you approve more spend."
420
+ )
421
+ _MAX_COST_HELP = (
422
+ "Halt the board once the estimated spend for this invocation reaches this many US dollars. "
423
+ "The estimate is the reported token total priced at --usd-per-million-tokens, so it is only "
424
+ "as good as that rate and only counts turns whose usage the provider actually reported. "
425
+ "Like --max-tokens the check runs between steps, so one step may overshoot. When the limit "
426
+ "stops a board the result says so and returns the resume command to continue after approval."
427
+ )
428
+ _USD_PER_MILLION_HELP = (
429
+ "The rate used to turn reported tokens into the estimated spend stored on every step and "
430
+ "returned with the board. It defaults to ten dollars per million tokens, which is a "
431
+ "placeholder rather than a quote for any specific model. Set it to your model's real "
432
+ "blended rate whenever you intend to rely on --max-cost, because the guard is only as "
433
+ "accurate as this number. It affects estimates and budget checks only and never changes "
434
+ "what anyone actually bills you."
435
+ )
436
+ _STATUS_ID_HELP = (
437
+ "The board id to read, as listed by the list subcommand and shown in every run result. It "
438
+ "names one directory inside the checkpoint root, so it is the whole address of a board "
439
+ "together with --checkpoint-root. There is no default, because reading the wrong board "
440
+ "silently is worse than being asked which one you meant. Pass the same id back to "
441
+ "show-step, fork, or a resuming run to keep working on that board."
442
+ )
443
+ _INDEX_HELP = (
444
+ "Which board step to read, given as its zero-based board position. The step must already "
445
+ "have a checkpoint file, so run status first if you are unsure which indices this board "
446
+ "stored. There is no default because there is no sensible step to guess at. The response "
447
+ "carries that step's whole record, including the exact prompt it received."
448
+ )
449
+ _FORK_AT_HELP = (
450
+ "How many leading steps to copy into the new board, so --at 70 copies steps 0 through 69. "
451
+ "The new board then continues from step 70 while the original keeps its own steps 70 "
452
+ "onward untouched, which is what makes two endings comparable. Every step below this index "
453
+ "must exist on disk, and the fork fails naming the first gap rather than copying a partial "
454
+ "prefix. Choose the index where the two experiments should diverge. On a --type dag board "
455
+ "the count follows the topological order the board runs in, matching what --from reads."
456
+ )
457
+ _FORK_INTO_HELP = (
458
+ "The board id the copied prefix is written to, inside the same checkpoint root. It has to "
459
+ "be a new name, because writing a fork over an existing board would destroy the history "
460
+ "that board already holds. Choose something you will recognize later, such as exp-b, since "
461
+ "this is the id you will pass to resume, status, and show-step from now on. The value "
462
+ "holds 1 to 64 letters, digits, dots, underscores, or hyphens."
463
+ )
464
+ _EXPORT_TO_HELP = (
465
+ "The file the board's task list is written to. A .md destination gets one level-two "
466
+ "section per task, and a .json destination gets a flat array of task strings; both reload "
467
+ "through --task-list to exactly the board that was exported. Parent directories are "
468
+ "created as needed and an existing file is overwritten, so point it somewhere deliberate. "
469
+ "There is no default, because writing a board to a guessed path is not recoverable."
470
+ )
471
+
472
+
473
+ class TaskBoardCommand:
474
+ """Keeps board validation and provider credentials ahead of paid admission."""
475
+
476
+ def register(self, parent: click.Group) -> None:
477
+ # Attaches the task-board group: one paid run verb plus the free, offline read verbs.
478
+ @parent.group(name="task-board", help=_GROUP_HELP)
479
+ def _board() -> None:
480
+ pass
481
+
482
+ self._register_run(_board)
483
+ self._register_list(_board)
484
+ self._register_status(_board)
485
+ self._register_show_step(_board)
486
+ self._register_fork(_board)
487
+ self._register_export_tasks(_board)
488
+
489
+ def _register_run(self, board: click.Group) -> None:
490
+ # Attaches run with its board source, context, agent, checkpoint, and budget options.
491
+ @board.command(name="run", help=_COMMAND_HELP)
492
+ @click.argument("tasks", nargs=-1)
493
+ @click.option(
494
+ "--task-file",
495
+ "task_files",
496
+ multiple=True,
497
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
498
+ help=_TASK_FILE_HELP,
499
+ )
500
+ @click.option(
501
+ "--task-list",
502
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
503
+ default=None,
504
+ help=_TASK_LIST_HELP,
505
+ )
506
+ @click.option(
507
+ "--host",
508
+ type=click.Choice(("auto", "codex")),
509
+ default="codex",
510
+ show_default=True,
511
+ help=_HOST_HELP,
512
+ )
513
+ @click.option(
514
+ "--window", type=click.IntRange(0, 25), default=10, show_default=True, help=_WINDOW_HELP
515
+ )
516
+ @click.option(
517
+ "--context-mode",
518
+ type=click.Choice(("windowed-summaries", "isolated")),
519
+ default="windowed-summaries",
520
+ show_default=True,
521
+ help=_CONTEXT_MODE_HELP,
522
+ )
523
+ @click.option(
524
+ "--handoff",
525
+ type=click.Choice(("summary", "task-and-summary", "full-result")),
526
+ default="summary",
527
+ show_default=True,
528
+ help=_HANDOFF_HELP,
529
+ )
530
+ @click.option(
531
+ "--summary-mode",
532
+ type=click.Choice(("truncate-tail", "head-tail")),
533
+ default="truncate-tail",
534
+ show_default=True,
535
+ help=_SUMMARY_MODE_HELP,
536
+ )
537
+ @click.option(
538
+ "--summary-max-chars",
539
+ type=click.IntRange(100, 8000),
540
+ default=1200,
541
+ show_default=True,
542
+ help=_SUMMARY_MAX_CHARS_HELP,
543
+ )
544
+ @click.option(
545
+ "--stop-on-error/--no-stop-on-error",
546
+ default=True,
547
+ show_default=True,
548
+ help=_STOP_ON_ERROR_HELP,
549
+ )
550
+ @click.option(
551
+ "--retries-per-task",
552
+ type=click.IntRange(0, 3),
553
+ default=1,
554
+ show_default=True,
555
+ help=_RETRIES_PER_TASK_HELP,
556
+ )
557
+ @click.option(
558
+ "--type",
559
+ "execution_type",
560
+ type=click.Choice(("linear", "dag")),
561
+ default="linear",
562
+ show_default=True,
563
+ help=_TYPE_HELP,
564
+ )
565
+ @click.option(
566
+ "--depends-on", "depends_on", multiple=True, default=(), help=_DEPENDS_ON_HELP
567
+ )
568
+ @click.option(
569
+ "--allow-decompose/--no-allow-decompose",
570
+ default=False,
571
+ show_default=True,
572
+ help=_ALLOW_DECOMPOSE_HELP,
573
+ )
574
+ @click.option(
575
+ "--max-subtasks",
576
+ type=click.IntRange(int(TaskBoardLimit.MIN_SUBTASKS), int(TaskBoardLimit.MAX_SUBTASKS)),
577
+ default=5,
578
+ show_default=True,
579
+ help=_MAX_SUBTASKS_HELP,
580
+ )
581
+ @click.option("--model", default="", help=_MODEL_HELP)
582
+ @click.option(
583
+ "--sandbox",
584
+ type=click.Choice(("read-only", "workspace-write", "full-access")),
585
+ default="workspace-write",
586
+ show_default=True,
587
+ help=_SANDBOX_HELP,
588
+ )
589
+ @click.option(
590
+ "--reasoning-effort",
591
+ type=click.Choice(("", "minimal", "low", "medium", "high", "xhigh")),
592
+ default="",
593
+ help=_REASONING_EFFORT_HELP,
594
+ )
595
+ @click.option(
596
+ "--turn-timeout",
597
+ type=click.IntRange(60, 5 * 24 * 60 * 60),
598
+ default=5 * 24 * 60 * 60,
599
+ show_default=True,
600
+ help=_TURN_TIMEOUT_HELP,
601
+ )
602
+ @click.option("--idempotency-key", "key", default=None, help=_IDEMPOTENCY_KEY_HELP)
603
+ @click.option(
604
+ "--checkpoint/--no-checkpoint", default=True, show_default=True, help=_CHECKPOINT_HELP
605
+ )
606
+ @click.option(
607
+ "--checkpoint-root",
608
+ type=click.Path(file_okay=False, path_type=Path),
609
+ default=_DEFAULT_CHECKPOINT_ROOT,
610
+ show_default=True,
611
+ help=_CHECKPOINT_ROOT_HELP,
612
+ )
613
+ @click.option("--checkpoint-id", default=None, help=_CHECKPOINT_ID_HELP)
614
+ @click.option(
615
+ "--checkpoint-mode",
616
+ type=click.Choice(("save-only", "stream", "export")),
617
+ default="save-only",
618
+ show_default=True,
619
+ help=_CHECKPOINT_MODE_HELP,
620
+ )
621
+ @click.option(
622
+ "--export-file",
623
+ type=click.Path(dir_okay=False, path_type=Path),
624
+ default=None,
625
+ help=_EXPORT_FILE_HELP,
626
+ )
627
+ @click.option(
628
+ "--report-file",
629
+ type=click.Path(dir_okay=False, path_type=Path),
630
+ default=None,
631
+ help=_REPORT_FILE_HELP,
632
+ )
633
+ @click.option("--on-checkpoint", default="", help=_ON_CHECKPOINT_HELP)
634
+ @click.option(
635
+ "--from",
636
+ "--resume-from",
637
+ "start_from",
638
+ type=click.IntRange(0),
639
+ default=None,
640
+ help=_FROM_HELP,
641
+ )
642
+ @click.option("--replay-task", type=click.IntRange(0), default=None, help=_REPLAY_TASK_HELP)
643
+ @click.option("--print-prompt", is_flag=True, default=False, help=_PRINT_PROMPT_HELP)
644
+ @click.option("--stop-after", type=click.IntRange(1), default=None, help=_STOP_AFTER_HELP)
645
+ @click.option(
646
+ "--retry-failed-only", is_flag=True, default=False, help=_RETRY_FAILED_ONLY_HELP
647
+ )
648
+ @click.option("--max-tokens", type=click.IntRange(1), default=None, help=_MAX_TOKENS_HELP)
649
+ @click.option(
650
+ "--max-cost",
651
+ type=click.FloatRange(min=0, min_open=True),
652
+ default=None,
653
+ help=_MAX_COST_HELP,
654
+ )
655
+ @click.option(
656
+ "--usd-per-million-tokens",
657
+ type=click.FloatRange(0),
658
+ default=10.0,
659
+ show_default=True,
660
+ help=_USD_PER_MILLION_HELP,
661
+ )
662
+ @click.pass_obj
663
+ def _run(ctx: Context, /, tasks: tuple[str, ...], **options: object) -> None:
664
+ # Groups the parsed values into the two frozen contracts the runner consumes, so
665
+ # the execution method takes bounded objects rather than thirty positional flags.
666
+ self.execute(ctx, tasks, options)
667
+
668
+ def _register_list(self, board: click.Group) -> None:
669
+ # Attaches the offline board index over one checkpoint root.
670
+ @board.command(name="list", help=_LIST_HELP)
671
+ @click.option(
672
+ "--checkpoint-root",
673
+ type=click.Path(file_okay=False, path_type=Path),
674
+ default=_DEFAULT_CHECKPOINT_ROOT,
675
+ show_default=True,
676
+ help=_CHECKPOINT_ROOT_HELP,
677
+ )
678
+ @click.pass_obj
679
+ def _list(ctx: Context, checkpoint_root: Path) -> None:
680
+ self.execute_list(ctx, checkpoint_root)
681
+
682
+ def _register_status(self, board: click.Group) -> None:
683
+ # Attaches the offline per-board progress report.
684
+ @board.command(name="status", help=_STATUS_HELP)
685
+ @click.option(
686
+ "--checkpoint-root",
687
+ type=click.Path(file_okay=False, path_type=Path),
688
+ default=_DEFAULT_CHECKPOINT_ROOT,
689
+ show_default=True,
690
+ help=_CHECKPOINT_ROOT_HELP,
691
+ )
692
+ @click.option("--checkpoint-id", required=True, help=_STATUS_ID_HELP)
693
+ @click.option(
694
+ "--report-file",
695
+ type=click.Path(dir_okay=False, path_type=Path),
696
+ default=None,
697
+ help=_REPORT_FILE_HELP,
698
+ )
699
+ @click.pass_obj
700
+ def _status(
701
+ ctx: Context, checkpoint_root: Path, checkpoint_id: str, report_file: Path | None
702
+ ) -> None:
703
+ self.execute_status(ctx, checkpoint_root, checkpoint_id, report_file)
704
+
705
+ def _register_show_step(self, board: click.Group) -> None:
706
+ # Attaches single-step inspection so debugging costs one call, not several file reads.
707
+ @board.command(name="show-step", help=_SHOW_STEP_HELP)
708
+ @click.option(
709
+ "--checkpoint-root",
710
+ type=click.Path(file_okay=False, path_type=Path),
711
+ default=_DEFAULT_CHECKPOINT_ROOT,
712
+ show_default=True,
713
+ help=_CHECKPOINT_ROOT_HELP,
714
+ )
715
+ @click.option("--checkpoint-id", required=True, help=_STATUS_ID_HELP)
716
+ @click.option("--index", type=click.IntRange(0), required=True, help=_INDEX_HELP)
717
+ @click.pass_obj
718
+ def _show_step(ctx: Context, checkpoint_root: Path, checkpoint_id: str, index: int) -> None:
719
+ self.execute_show_step(ctx, checkpoint_root, checkpoint_id, index)
720
+
721
+ def _register_fork(self, board: click.Group) -> None:
722
+ # Attaches prefix copying, which branches a board without touching the original.
723
+ @board.command(name="fork", help=_FORK_HELP)
724
+ @click.option(
725
+ "--checkpoint-root",
726
+ type=click.Path(file_okay=False, path_type=Path),
727
+ default=_DEFAULT_CHECKPOINT_ROOT,
728
+ show_default=True,
729
+ help=_CHECKPOINT_ROOT_HELP,
730
+ )
731
+ @click.option("--checkpoint-id", required=True, help=_STATUS_ID_HELP)
732
+ @click.option("--at", "at_index", type=click.IntRange(0), required=True, help=_FORK_AT_HELP)
733
+ @click.option("--into", required=True, help=_FORK_INTO_HELP)
734
+ @click.pass_obj
735
+ def _fork(
736
+ ctx: Context, checkpoint_root: Path, checkpoint_id: str, at_index: int, into: str
737
+ ) -> None:
738
+ self.execute_fork(ctx, checkpoint_root, checkpoint_id, at_index, into)
739
+
740
+ def _register_export_tasks(self, board: click.Group) -> None:
741
+ # Attaches task-list export, the inverse of --task-list import.
742
+ @board.command(name="export-tasks", help=_EXPORT_TASKS_HELP)
743
+ @click.option(
744
+ "--checkpoint-root",
745
+ type=click.Path(file_okay=False, path_type=Path),
746
+ default=_DEFAULT_CHECKPOINT_ROOT,
747
+ show_default=True,
748
+ help=_CHECKPOINT_ROOT_HELP,
749
+ )
750
+ @click.option("--checkpoint-id", required=True, help=_STATUS_ID_HELP)
751
+ @click.option(
752
+ "--to",
753
+ "destination",
754
+ type=click.Path(dir_okay=False, path_type=Path),
755
+ required=True,
756
+ help=_EXPORT_TO_HELP,
757
+ )
758
+ @click.pass_obj
759
+ def _export(
760
+ ctx: Context, checkpoint_root: Path, checkpoint_id: str, destination: Path
761
+ ) -> None:
762
+ self.execute_export_tasks(ctx, checkpoint_root, checkpoint_id, destination)
763
+
764
+ def execute(
765
+ self, context: Context, tasks: tuple[str, ...], options: Mapping[str, object]
766
+ ) -> None:
767
+ # Everything before ADMISSION is free, so every rejection a caller can cause happens
768
+ # before the wallet is touched, and execution happens only after the grant is verified.
769
+ parsed = TaskBoardOptions(options)
770
+ progress = context.output().diagnostic
771
+ progress(Progress.PREPARING)
772
+ # The key is validated first because an invalid one would otherwise surface only after
773
+ # the board has been read from disk and a host has been resolved.
774
+ key = self._idempotency_key(parsed.key)
775
+ # Checkpoint storage is resolved before the board is read, because a resume with no
776
+ # task source of its own gets its board back out of the stored manifest.
777
+ checkpointer = self._checkpointer(parsed)
778
+ # Resolving the board is the one step that reads caller files; it fails closed on a
779
+ # mixed, empty, non-Markdown, or unreadable source.
780
+ stored = checkpointer if parsed.resumes else None
781
+ board = self._board_tasks(tasks, parsed.task_files, parsed.task_list, stored)
782
+ settings = self._settings(board, parsed)
783
+ if settings.allow_decompose:
784
+ progress(Progress.DECOMPOSE_ISOLATED)
785
+ controls = parsed.controls()
786
+ checkpointer = self._identified(checkpointer, parsed, board)
787
+ # A prompt preview never starts an agent, so it returns before credentials or payment.
788
+ if parsed.print_prompt:
789
+ self._preview(context, checkpointer, settings, controls)
790
+ return
791
+ # Proving the stored prefix loads before admission is what stops a doomed resume paying.
792
+ self._validate_resume(checkpointer, settings, parsed)
793
+ requested = None if parsed.host == "auto" else RuntimeHost(parsed.host)
794
+ # The plan carries only a board label, never the task text: task content is local and
795
+ # must not travel to the backend with the admission request.
796
+ plan = context.runtime_launch_planner().build_task_board(board, requested, Path.cwd())
797
+ progress(Progress.CREDENTIALS)
798
+ # Building the session imports the SDK and filters the child environment, so a missing
799
+ # SDK or a missing provider key also fails before payment.
800
+ session = self._session(context, checkpointer, controls)
801
+ session.prepare(plan)
802
+ endpoints = context.runtime_endpoints()
803
+ progress(Progress.ADMISSION)
804
+ # One flat admission covers the whole board, however many tasks it holds.
805
+ grant = endpoints.admit_task_board(RuntimeAdmissionRequest(host=plan.host), key)
806
+ if grant.grant_token is None:
807
+ raise RuntimeAdmissionNotVerified("grant_token_missing")
808
+ progress(Progress.VERIFYING)
809
+ # The grant is re-read from the backend rather than trusted as returned, and the key
810
+ # hash ties that verification to this exact invocation.
811
+ verified = endpoints.verify_grant(
812
+ RuntimeGrantVerificationRequest(
813
+ grant_token=grant.grant_token,
814
+ idempotency_key_hash=RuntimeAdmissionGate.hash_idempotency_key(key),
815
+ )
816
+ )
817
+ verdict = RuntimeAdmissionGate().verify_online(plan, grant, verified)
818
+ if not verdict.admitted:
819
+ raise RuntimeAdmissionNotVerified(verdict.reason)
820
+ progress(Progress.ADMITTED)
821
+ # The runner receives the verified verdict and no endpoints at all, which is what keeps
822
+ # the network out of task execution.
823
+ result = context.runtime_executor().execute_task_board(plan, settings, session, verdict)
824
+ # Only the board result reaches stdout; every phase line above went to stderr.
825
+ context.output().result(
826
+ OutputDocument(kind="runtime.task-board", data=result.model_dump(mode="json")),
827
+ result.text,
828
+ )
829
+
830
+ def execute_list(self, context: Context, checkpoint_root: Path) -> None:
831
+ # Offline and free: one row per board directory holding a manifest. Only a root that
832
+ # cannot be scanned fails the verb, naming --checkpoint-root; a board that will not
833
+ # load is listed with its own problem and fix instead, so one corrupt file never hides
834
+ # every other board from the caller deciding what to do next.
835
+ root = self._root(checkpoint_root)
836
+ try:
837
+ index = TaskBoardCheckpointer.list_boards(root)
838
+ except LocalFileReadFailed as error:
839
+ raise TaskBoardCheckpointRootUnreadable(error.reason) from error
840
+ rows = [
841
+ f"{item.board_id} {item.completed}/{item.task_count} done, "
842
+ f"{item.failed} failed, {item.pending} pending {item.board_dir}"
843
+ for item in index.boards
844
+ ]
845
+ rows.extend(
846
+ f"{item.board_id} unreadable: {item.problem} {item.hint} {item.board_dir}"
847
+ for item in index.unreadable
848
+ )
849
+ empty = (
850
+ f"No checkpointed boards under {root}. Pass --checkpoint-root if the boards were "
851
+ "saved somewhere else."
852
+ )
853
+ listing = TaskBoardListing(
854
+ checkpoint_root=str(root),
855
+ boards=index.boards,
856
+ unreadable=index.unreadable,
857
+ text="\n".join(rows) if rows else empty,
858
+ )
859
+ context.output().result(
860
+ OutputDocument(kind="runtime.task-board.list", data=listing.model_dump(mode="json")),
861
+ listing.text,
862
+ )
863
+
864
+ def execute_status(
865
+ self, context: Context, checkpoint_root: Path, board_id: str, report_file: Path | None
866
+ ) -> None:
867
+ # Offline and free, so an agent can call it between every decision it makes. Reading
868
+ # the board fails with the checkpointer's own board-specific failures, which already
869
+ # name the board, the step, and the repair. Only the report write is put in context
870
+ # here, because only this verb knows the destination came from --report-file.
871
+ checkpointer = self._read_only(checkpoint_root, board_id)
872
+ chain = checkpointer.chain()
873
+ spend = sum(node.estimated_cost_usd or 0.0 for node in chain.nodes)
874
+ try:
875
+ report = (
876
+ None if report_file is None else str(checkpointer.write_report(chain, report_file))
877
+ )
878
+ except LocalFileWriteFailed as error:
879
+ raise TaskBoardOutputWriteFailed("--report-file", error.reason) from error
880
+ except (OSError, RuntimeError) as error:
881
+ # Expanding ~ or resolving the destination fails before the store is reached.
882
+ reason = LocalFileStore.reason_for(error)
883
+ raise TaskBoardOutputWriteFailed("--report-file", reason) from error
884
+ status = TaskBoardStatusReport(
885
+ board_id=chain.board_id,
886
+ board_dir=chain.board_dir,
887
+ task_count=chain.task_count,
888
+ completed_indices=chain.completed_indices,
889
+ failed_indices=chain.failed_indices,
890
+ pending_indices=chain.pending_indices,
891
+ total_tokens=chain.total_tokens,
892
+ estimated_cost_usd=round(spend, 6),
893
+ parent_board_id=chain.parent_board_id,
894
+ report_file=report,
895
+ resume_command=self._resume_command(checkpointer, chain),
896
+ text=(
897
+ f"{chain.board_id}: {len(chain.completed_indices)} completed, "
898
+ f"{len(chain.failed_indices)} failed, {len(chain.pending_indices)} pending "
899
+ f"of {chain.task_count} in {chain.board_dir}"
900
+ ),
901
+ )
902
+ context.output().result(
903
+ OutputDocument(kind="runtime.task-board.status", data=status.model_dump(mode="json")),
904
+ status.text,
905
+ )
906
+
907
+ def execute_show_step(
908
+ self, context: Context, checkpoint_root: Path, board_id: str, index: int
909
+ ) -> None:
910
+ # Returns the whole stored record, so a caller never has to open a checkpoint file. The
911
+ # chain is read first because the task count is what tells an out-of-range index apart
912
+ # from a step that is merely pending, and those two need different next calls.
913
+ checkpointer = self._read_only(checkpoint_root, board_id)
914
+ chain = checkpointer.chain()
915
+ try:
916
+ record = checkpointer.read_step(index)
917
+ except TaskBoardCheckpointMissing as error:
918
+ # The shared missing-step failure is worded for a resume; here the fix is a
919
+ # different --index, so it is replaced by one that says which indices exist.
920
+ raise TaskBoardStepNotStored(checkpointer.board_id, index, chain.task_count) from error
921
+ detail = TaskBoardStepDetail(
922
+ board_id=checkpointer.board_id,
923
+ board_dir=str(checkpointer.directory),
924
+ step_file=str(checkpointer.step_file(index)),
925
+ step=record,
926
+ ancestors=tuple(node.index for node in chain.ancestors_of(index)),
927
+ text=f"[{record.index}] {record.status}: {record.summary}",
928
+ )
929
+ context.output().result(
930
+ OutputDocument(kind="runtime.task-board.step", data=detail.model_dump(mode="json")),
931
+ detail.text,
932
+ )
933
+
934
+ def execute_fork(
935
+ self, context: Context, checkpoint_root: Path, board_id: str, at_index: int, into: str
936
+ ) -> None:
937
+ # Copies a prefix into a new board; the source board is only ever read. Every check
938
+ # that can refuse the fork runs before the first byte is copied: the new id must be
939
+ # safe, the source must load, the target must be unused, and fork_into proves the
940
+ # prefix is stored. What remains is a filesystem failure mid-copy, which names the
941
+ # partial directory to delete.
942
+ self._require_board_id(into, "--into")
943
+ source = self._read_only(checkpoint_root, board_id)
944
+ target = TaskBoardCheckpointer(self._root(checkpoint_root), into)
945
+ if target.directory.exists():
946
+ raise TaskBoardForkTargetExists(into)
947
+ try:
948
+ copied = source.fork_into(target, at_index)
949
+ except LocalFileWriteFailed as error:
950
+ raise TaskBoardForkFailed(into, error.reason) from error
951
+ fork = TaskBoardForkResult(
952
+ board_id=into,
953
+ board_dir=str(target.directory),
954
+ parent_board_id=source.board_id,
955
+ forked_at_index=at_index,
956
+ copied_steps=copied,
957
+ resume_command=target.resume_command(at_index),
958
+ text=f"Forked {source.board_id} at step {at_index} into {target.directory}",
959
+ )
960
+ context.output().result(
961
+ OutputDocument(kind="runtime.task-board.fork", data=fork.model_dump(mode="json")),
962
+ fork.text,
963
+ )
964
+
965
+ def execute_export_tasks(
966
+ self, context: Context, checkpoint_root: Path, board_id: str, destination: Path
967
+ ) -> None:
968
+ # Writes the stored board back out in a shape --task-list reads back identically. A
969
+ # manifest with no task list fails in stored_tasks and an unknown suffix fails in
970
+ # write_task_list, each with its own failure; a refused write is named against --to.
971
+ checkpointer = self._read_only(checkpoint_root, board_id)
972
+ board = checkpointer.stored_tasks()
973
+ try:
974
+ written = TaskBoardTaskFiles().write_task_list(
975
+ destination.expanduser().resolve(), board
976
+ )
977
+ except LocalFileWriteFailed as error:
978
+ raise TaskBoardOutputWriteFailed("--to", error.reason) from error
979
+ except (OSError, RuntimeError) as error:
980
+ # Expanding ~ or resolving the destination fails before the store is reached.
981
+ raise TaskBoardOutputWriteFailed("--to", LocalFileStore.reason_for(error)) from error
982
+ export = TaskBoardTaskExport(
983
+ board_id=checkpointer.board_id,
984
+ path=str(written),
985
+ task_count=len(board),
986
+ text=f"Wrote {len(board)} tasks to {written}",
987
+ )
988
+ context.output().result(
989
+ OutputDocument(kind="runtime.task-board.tasks", data=export.model_dump(mode="json")),
990
+ export.text,
991
+ )
992
+
993
+ def _preview(
994
+ self,
995
+ context: Context,
996
+ checkpointer: TaskBoardCheckpointer | None,
997
+ settings: TaskBoardSettings,
998
+ controls: TaskBoardRunControls,
999
+ ) -> None:
1000
+ # The one run invocation that never pays: it rebuilds a prompt and returns it.
1001
+ if checkpointer is None or controls.replay_index is None:
1002
+ raise click.BadParameter("--print-prompt requires --replay-task and checkpointing.")
1003
+ session = TaskBoardCodexSession({}, context.output().diagnostic)
1004
+ session.with_checkpoints(checkpointer, controls)
1005
+ prompt = session.preview_prompt(settings, controls.replay_index)
1006
+ preview = TaskBoardPromptPreview(
1007
+ board_id=checkpointer.board_id,
1008
+ board_dir=str(checkpointer.directory),
1009
+ index=controls.replay_index,
1010
+ prompt=prompt,
1011
+ text=prompt,
1012
+ )
1013
+ context.output().result(
1014
+ OutputDocument(kind="runtime.task-board.prompt", data=preview.model_dump(mode="json")),
1015
+ preview.text,
1016
+ )
1017
+
1018
+ def _read_only(self, checkpoint_root: Path, board_id: str) -> TaskBoardCheckpointer:
1019
+ # Every offline verb addresses a board the same way and proves it exists before use:
1020
+ # an unsafe id, an unresolvable root, an absent board, and a broken manifest each fail
1021
+ # here with their own typed failure, before the verb reads anything further.
1022
+ self._require_board_id(board_id, "--checkpoint-id")
1023
+ checkpointer = TaskBoardCheckpointer(self._root(checkpoint_root), board_id)
1024
+ checkpointer.read_manifest()
1025
+ return checkpointer
1026
+
1027
+ def _root(self, checkpoint_root: Path) -> Path:
1028
+ # Absolute from here on. Expanding ~ or resolving a link loop can fail, and that is a
1029
+ # bad --checkpoint-root rather than a CLI defect, so it is named as one.
1030
+ try:
1031
+ return checkpoint_root.expanduser().resolve()
1032
+ except (OSError, RuntimeError) as error:
1033
+ raise TaskBoardCheckpointRootUnreadable(LocalFileStore.reason_for(error)) from error
1034
+
1035
+ def _require_board_id(self, board_id: str, option: str) -> None:
1036
+ # The character class lives with the board layout; this names the option to fix.
1037
+ if not TaskBoardCheckpointer.valid_id(board_id):
1038
+ raise TaskBoardBoardIdInvalid(option)
1039
+
1040
+ def _resume_command(
1041
+ self, checkpointer: TaskBoardCheckpointer, chain: TaskBoardCheckpointChain
1042
+ ) -> str:
1043
+ # Points at the first unfinished step, or at repair when only failures remain, so the
1044
+ # command a caller pastes back never re-pays for steps that already completed. The
1045
+ # point is a position in the board's execution order, which is what --from reads.
1046
+ if chain.failed_indices:
1047
+ return checkpointer.resume_command(0, repair=True)
1048
+ pending = set(chain.pending_indices)
1049
+ order = checkpointer.execution_order()
1050
+ following = next((rank for rank, index in enumerate(order) if index in pending), None)
1051
+ return checkpointer.resume_command(chain.task_count if following is None else following)
1052
+
1053
+ def _board_tasks(
1054
+ self,
1055
+ tasks: tuple[str, ...],
1056
+ task_files: tuple[Path, ...],
1057
+ task_list: Path | None = None,
1058
+ stored: TaskBoardCheckpointer | None = None,
1059
+ ) -> tuple[str, ...]:
1060
+ # A board comes from exactly one source: literal task strings, whole Markdown files one
1061
+ # task each, one task-list file, or the list a stored board recorded. Accepting two at
1062
+ # once would leave the board's order ambiguous.
1063
+ files = TaskBoardTaskFiles()
1064
+ literals = tuple(item for item in (task.strip() for task in tasks) if item)
1065
+ if sum((bool(literals), bool(task_files), task_list is not None)) > 1:
1066
+ raise TaskBoardInputInvalid()
1067
+ if literals:
1068
+ return literals
1069
+ if task_files:
1070
+ return tuple(files.read_task_file(path) for path in task_files)
1071
+ if task_list is not None:
1072
+ return files.read_task_list(task_list)
1073
+ if stored is not None:
1074
+ return stored.stored_tasks()
1075
+ raise TaskBoardInputInvalid()
1076
+
1077
+ def _settings(self, board: tuple[str, ...], parsed: TaskBoardOptions) -> TaskBoardSettings:
1078
+ # Constructs frozen board settings with validated context, handoff, agent, and
1079
+ # dependency behavior. Links are parsed against the resolved board size, and a cycle
1080
+ # is caught here, so every bad link fails before credentials, payment, or a host.
1081
+ links = self._parse_dependencies(parsed.depends_on, parsed.execution_type, len(board))
1082
+ if links and TaskBoardSettings.topological_order(len(board), links) is None:
1083
+ raise TaskBoardDependencyInvalid()
1084
+ if parsed.allow_decompose and parsed.execution_type == "dag":
1085
+ raise TaskBoardDecomposeInvalid("--type dag")
1086
+ if parsed.allow_decompose and parsed.checkpoint:
1087
+ raise TaskBoardDecomposeInvalid("checkpointing, which stays on unless --no-checkpoint")
1088
+ return TaskBoardSettings(
1089
+ tasks=board,
1090
+ window=parsed.window,
1091
+ context_mode=TaskBoardContextMode(parsed.context_mode),
1092
+ handoff_mode=TaskBoardHandoffMode(parsed.handoff),
1093
+ summary_mode=TaskBoardSummaryMode(parsed.summary_mode),
1094
+ summary_max_chars=parsed.summary_max_chars,
1095
+ stop_on_error=parsed.stop_on_error,
1096
+ max_retries_per_task=parsed.retries_per_task,
1097
+ execution_type=TaskBoardExecutionType(parsed.execution_type),
1098
+ dependencies=links,
1099
+ agent=parsed.agent(),
1100
+ allow_decompose=parsed.allow_decompose,
1101
+ max_subtasks=parsed.max_subtasks,
1102
+ )
1103
+
1104
+ def _parse_dependencies(
1105
+ self, raw: tuple[str, ...], execution_type: str, task_count: int
1106
+ ) -> tuple[tuple[int, int], ...]:
1107
+ # Normalizes every link flag into sorted unique child-parent pairs.
1108
+ if execution_type != "dag" and raw:
1109
+ raise TaskBoardDependencyInvalid()
1110
+ pairs: list[tuple[int, int]] = []
1111
+ for spec in raw:
1112
+ pairs.extend(self._parse_single_link(spec, task_count))
1113
+ if len(set(pairs)) != len(pairs):
1114
+ raise TaskBoardDependencyInvalid()
1115
+ return tuple(sorted(pairs))
1116
+
1117
+ def _parse_single_link(self, spec: str, task_count: int) -> tuple[tuple[int, int], ...]:
1118
+ # Parses one CHILD:PARENT[,PARENT...] flag into its child-parent pairs.
1119
+ halves = spec.split(":")
1120
+ if len(halves) != 2:
1121
+ raise TaskBoardDependencyInvalid()
1122
+ child_text, parents_text = halves[0].strip(), halves[1].strip()
1123
+ if not child_text.isdigit() or not parents_text:
1124
+ raise TaskBoardDependencyInvalid()
1125
+ child = int(child_text)
1126
+ pairs: list[tuple[int, int]] = []
1127
+ for parent_text in parents_text.split(","):
1128
+ cleaned = parent_text.strip()
1129
+ if not cleaned.isdigit():
1130
+ raise TaskBoardDependencyInvalid()
1131
+ parent = int(cleaned)
1132
+ if child >= task_count or parent >= task_count or child == parent:
1133
+ raise TaskBoardDependencyInvalid()
1134
+ pairs.append((child, parent))
1135
+ if not pairs:
1136
+ raise TaskBoardDependencyInvalid()
1137
+ return tuple(pairs)
1138
+
1139
+ def _checkpointer(self, parsed: TaskBoardOptions) -> TaskBoardCheckpointer | None:
1140
+ # Derives the board directory without touching disk; resume flags require storage. The
1141
+ # id may still be unknown here, because a hashed id needs the board that is read next.
1142
+ if parsed.start_from is not None and parsed.replay_task is not None:
1143
+ raise click.BadParameter("--from and --replay-task cannot be combined.")
1144
+ if not parsed.checkpoint:
1145
+ if parsed.resumes or parsed.checkpoint_id is not None or parsed.report_file:
1146
+ raise click.BadParameter("Resume options require checkpointing to stay enabled.")
1147
+ return None
1148
+ if parsed.checkpoint_id is None:
1149
+ return None
1150
+ self._require_board_id(parsed.checkpoint_id, "--checkpoint-id")
1151
+ return TaskBoardCheckpointer(parsed.root(), parsed.checkpoint_id)
1152
+
1153
+ def _identified(
1154
+ self,
1155
+ checkpointer: TaskBoardCheckpointer | None,
1156
+ parsed: TaskBoardOptions,
1157
+ board: tuple[str, ...],
1158
+ ) -> TaskBoardCheckpointer | None:
1159
+ # An unnamed board is addressed by the hash of its task list, which is only knowable
1160
+ # once the board itself has been resolved from whichever source supplied it.
1161
+ if checkpointer is not None or not parsed.checkpoint:
1162
+ return checkpointer
1163
+ return TaskBoardCheckpointer(parsed.root(), TaskBoardCheckpointer.board_id_for(board))
1164
+
1165
+ def _validate_resume(
1166
+ self,
1167
+ checkpointer: TaskBoardCheckpointer | None,
1168
+ settings: TaskBoardSettings,
1169
+ parsed: TaskBoardOptions,
1170
+ ) -> None:
1171
+ # Proves the stored prefix loads before admission so a doomed resume never pays.
1172
+ if checkpointer is None:
1173
+ return
1174
+ board_size = len(settings.tasks)
1175
+ if parsed.start_from is not None and parsed.start_from > board_size:
1176
+ raise click.BadParameter(f"--from {parsed.start_from} is past this board.")
1177
+ if parsed.replay_task is not None and parsed.replay_task >= board_size:
1178
+ raise click.BadParameter(f"--replay-task {parsed.replay_task} is past this board.")
1179
+ if parsed.retry_failed_only:
1180
+ checkpointer.validate_manifest(settings)
1181
+ return
1182
+ # Both prefixes count steps in execution order, which on a DAG board is not board order.
1183
+ order = settings.execution_order()
1184
+ if parsed.start_from:
1185
+ checkpointer.validate_manifest(settings)
1186
+ checkpointer.load_prefix(parsed.start_from, order)
1187
+ elif parsed.replay_task is not None:
1188
+ checkpointer.validate_manifest(settings)
1189
+ checkpointer.load_prefix(order.index(parsed.replay_task), order)
1190
+
1191
+ def _idempotency_key(self, key: str | None) -> str:
1192
+ # Validates the replay-safe admission key before any local planning.
1193
+ candidate = key or str(uuid4())
1194
+ if re.fullmatch(r"[A-Za-z0-9._:-]{8,128}", candidate) is None:
1195
+ raise click.BadParameter(
1196
+ "Use 8–128 letters, digits, dots, underscores, colons or hyphens."
1197
+ )
1198
+ return candidate
1199
+
1200
+ def _session(
1201
+ self,
1202
+ context: Context,
1203
+ checkpointer: TaskBoardCheckpointer | None,
1204
+ controls: TaskBoardRunControls,
1205
+ ) -> TaskBoardCodexSession:
1206
+ # Filters parent secrets so the child Codex process inherits only its key.
1207
+ credentials = context.require_provider_credentials(Provider.OPENAI)
1208
+ environment = dict(context.environment)
1209
+ for name in (
1210
+ *PROVIDER_ENV_VARS.values(),
1211
+ "GOOGLE_API_KEY",
1212
+ "VIDBYTE_API_KEY",
1213
+ "RUNTIME_ADMISSION_SIGNING_KEY",
1214
+ "CODEX_API_KEY",
1215
+ ):
1216
+ environment[name] = ""
1217
+ environment["OPENAI_API_KEY"] = credentials.secret_value()
1218
+ session = TaskBoardCodexSession(
1219
+ environment, context.output().diagnostic, self._streamer(context)
1220
+ )
1221
+ if checkpointer is not None:
1222
+ session.with_checkpoints(checkpointer, controls)
1223
+ return session
1224
+
1225
+ def _streamer(self, context: Context) -> Callable[[TaskBoardCheckpoint], None]:
1226
+ # Streamed steps are transitions, not the final result, so a --json consumer still
1227
+ # sees exactly one document while a --jsonl consumer sees each step as it lands.
1228
+ def emit(record: TaskBoardCheckpoint) -> None:
1229
+ context.output().transition(
1230
+ OutputDocument(kind="runtime.task-board.step", data=record.model_dump(mode="json")),
1231
+ f"[{record.index}] {record.status}: {record.summary}",
1232
+ )
1233
+
1234
+ return emit
1235
+
1236
+
1237
+ class TaskBoardOptions:
1238
+ """The parsed run invocation: board sources, board settings, and this run's controls."""
1239
+
1240
+ def __init__(self, options: Mapping[str, object]) -> None:
1241
+ # Click has already type-checked every value, so this only names them and fixes types.
1242
+ self.task_files = self._paths(options["task_files"])
1243
+ self.task_list = self._path(options["task_list"])
1244
+ self.host = str(options["host"])
1245
+ self.window = int(str(options["window"]))
1246
+ self.context_mode = str(options["context_mode"])
1247
+ self.handoff = str(options["handoff"])
1248
+ self.summary_mode = str(options["summary_mode"])
1249
+ self.summary_max_chars = int(str(options["summary_max_chars"]))
1250
+ self.stop_on_error = bool(options["stop_on_error"])
1251
+ self.retries_per_task = int(str(options["retries_per_task"]))
1252
+ self.execution_type = str(options["execution_type"])
1253
+ self.depends_on = tuple(str(item) for item in self._items(options["depends_on"]))
1254
+ self.allow_decompose = bool(options.get("allow_decompose", False))
1255
+ self.max_subtasks = int(str(options.get("max_subtasks", 5)))
1256
+ self.model = str(options["model"])
1257
+ self.sandbox = str(options["sandbox"])
1258
+ self.reasoning_effort = str(options["reasoning_effort"])
1259
+ self.turn_timeout = int(str(options["turn_timeout"]))
1260
+ self.key = self._text(options["key"])
1261
+ self.checkpoint = bool(options["checkpoint"])
1262
+ self.checkpoint_root = self._paths((options["checkpoint_root"],))[0]
1263
+ self.checkpoint_id = self._text(options["checkpoint_id"])
1264
+ self.checkpoint_mode = str(options["checkpoint_mode"])
1265
+ self.export_file = self._path(options["export_file"])
1266
+ self.report_file = self._path(options["report_file"])
1267
+ self.on_checkpoint = str(options["on_checkpoint"])
1268
+ self.start_from = self._number(options["start_from"])
1269
+ self.replay_task = self._number(options["replay_task"])
1270
+ self.print_prompt = bool(options["print_prompt"])
1271
+ self.stop_after = self._number(options["stop_after"])
1272
+ self.retry_failed_only = bool(options["retry_failed_only"])
1273
+ self.max_tokens = self._number(options["max_tokens"])
1274
+ self.max_cost = self._decimal(options["max_cost"])
1275
+ self.usd_per_million_tokens = float(str(options["usd_per_million_tokens"]))
1276
+
1277
+ @property
1278
+ def resumes(self) -> bool:
1279
+ # Whether this invocation reads a stored board rather than starting a fresh one.
1280
+ return self.start_from is not None or self.replay_task is not None or self.retry_failed_only
1281
+
1282
+ def root(self) -> Path:
1283
+ # Absolute from here on, which is what makes a returned board directory portable.
1284
+ return self.checkpoint_root.expanduser().resolve()
1285
+
1286
+ def agent(self) -> TaskBoardAgentSettings:
1287
+ # The Codex configuration every iteration of this board is constructed with.
1288
+ return TaskBoardAgentSettings(
1289
+ model=self.model,
1290
+ sandbox=TaskBoardSandbox(self.sandbox),
1291
+ reasoning_effort=TaskBoardReasoningEffort(self.reasoning_effort),
1292
+ turn_timeout_seconds=self.turn_timeout,
1293
+ )
1294
+
1295
+ def controls(self) -> TaskBoardRunControls:
1296
+ # Everything that varies per invocation rather than per board, kept out of the board
1297
+ # fingerprint so a resume with a different budget is not mistaken for a different board.
1298
+ return TaskBoardRunControls(
1299
+ checkpoint_mode=TaskBoardCheckpointMode(self.checkpoint_mode),
1300
+ export_file="" if self.export_file is None else str(self.export_file),
1301
+ report_file="" if self.report_file is None else str(self.report_file),
1302
+ on_checkpoint=self.on_checkpoint,
1303
+ start_from=self.start_from or 0,
1304
+ replay_index=self.replay_task,
1305
+ stop_after=self.stop_after,
1306
+ retry_failed_only=self.retry_failed_only,
1307
+ max_tokens=self.max_tokens,
1308
+ max_cost_usd=self.max_cost,
1309
+ usd_per_million_tokens=self.usd_per_million_tokens,
1310
+ )
1311
+
1312
+ @staticmethod
1313
+ def _paths(value: object) -> tuple[Path, ...]:
1314
+ return (
1315
+ tuple(item for item in value if isinstance(item, Path))
1316
+ if isinstance(value, tuple)
1317
+ else ()
1318
+ )
1319
+
1320
+ @staticmethod
1321
+ def _items(value: object) -> tuple[object, ...]:
1322
+ return value if isinstance(value, tuple) else ()
1323
+
1324
+ @staticmethod
1325
+ def _path(value: object) -> Path | None:
1326
+ return value if isinstance(value, Path) else None
1327
+
1328
+ @staticmethod
1329
+ def _text(value: object) -> str | None:
1330
+ return value if isinstance(value, str) else None
1331
+
1332
+ @staticmethod
1333
+ def _number(value: object) -> int | None:
1334
+ return value if isinstance(value, int) and not isinstance(value, bool) else None
1335
+
1336
+ @staticmethod
1337
+ def _decimal(value: object) -> float | None:
1338
+ return (
1339
+ float(value)
1340
+ if isinstance(value, (int, float)) and not isinstance(value, bool)
1341
+ else None
1342
+ )