codee-agent 0.7.1__tar.gz → 0.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. {codee_agent-0.7.1 → codee_agent-0.7.3}/PKG-INFO +1 -1
  2. {codee_agent-0.7.1 → codee_agent-0.7.3}/pyproject.toml +1 -1
  3. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/admin.py +91 -41
  4. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/admin_service.py +7 -0
  5. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/executor.py +8 -0
  6. codee_agent-0.7.3/src/codee/lib/runtime_control.py +21 -0
  7. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_admin_service.py +11 -0
  8. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_executor.py +21 -0
  9. {codee_agent-0.7.1 → codee_agent-0.7.3}/LICENSE +0 -0
  10. {codee_agent-0.7.1 → codee_agent-0.7.3}/README.md +0 -0
  11. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/.gitignore +0 -0
  12. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/__init__.py +0 -0
  13. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/admin_api.py +0 -0
  14. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/admin_cli.py +0 -0
  15. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/agent_cli.py +0 -0
  16. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/coding_agents.py +0 -0
  17. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/init_cli.py +0 -0
  18. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/__init__.py +0 -0
  19. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/claude_key_rotation.py +0 -0
  20. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/cron_describe.py +0 -0
  21. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/mcp_config.py +0 -0
  22. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/runs_db.py +0 -0
  23. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/test_claude_key_rotation.py +0 -0
  24. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/test_mcp_config.py +0 -0
  25. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/test_runs_db.py +0 -0
  26. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/test_trigger_cron_skills.py +0 -0
  27. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/test_trigger_issue_skills.py +0 -0
  28. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
  29. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/trigger_cron_skills.py +0 -0
  30. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/trigger_email_skills.py +0 -0
  31. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/lib/trigger_issue_skills.py +0 -0
  32. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/mail_server.py +0 -0
  33. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/setup_wizard.py +0 -0
  34. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/start_cli.py +0 -0
  35. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/tasks_providers.py +0 -0
  36. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/AGENTS.md +0 -0
  37. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/CLAUDE.md +0 -0
  38. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
  39. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
  40. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
  41. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
  42. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
  43. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
  44. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
  45. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
  46. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
  47. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
  48. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
  49. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
  50. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_admin_api.py +0 -0
  51. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_admin_cli.py +0 -0
  52. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_agent_cli.py +0 -0
  53. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_coding_agents.py +0 -0
  54. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_init_cli.py +0 -0
  55. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_memory_index.py +0 -0
  56. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_setup_wizard.py +0 -0
  57. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/test_start_cli.py +0 -0
  58. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee/workflow_graph.py +0 -0
  59. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_admin/__init__.py +0 -0
  60. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_admin/codee_admin.py +0 -0
  61. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_abstract/__init__.py +0 -0
  62. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_abstract/provider.py +0 -0
  63. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_claude_code/__init__.py +0 -0
  64. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_claude_code/account.py +0 -0
  65. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_claude_code/credentials.py +0 -0
  66. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_claude_code/oauth.py +0 -0
  67. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_claude_code/provider.py +0 -0
  68. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_claude_code/test.py +0 -0
  69. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_claude_code/usage.py +0 -0
  70. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_codex/__init__.py +0 -0
  71. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_codex/provider.py +0 -0
  72. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_codex/test.py +0 -0
  73. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_github_copilot/__init__.py +0 -0
  74. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_github_copilot/provider.py +0 -0
  75. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_github_copilot/test.py +0 -0
  76. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_opencode/__init__.py +0 -0
  77. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_opencode/provider.py +0 -0
  78. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_agent_opencode/test.py +0 -0
  79. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_database/__init__.py +0 -0
  80. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_database/claude_code_accounts.py +0 -0
  81. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_database/database.py +0 -0
  82. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_database/oauth_tokens.py +0 -0
  83. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_main_context/__init__.py +0 -0
  84. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_main_context/context.py +0 -0
  85. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_main_context/logging.py +0 -0
  86. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_main_context/test_context.py +0 -0
  87. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_main_context/test_logging.py +0 -0
  88. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_abstract/__init__.py +0 -0
  89. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_abstract/provider.py +0 -0
  90. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_azure_devops/__init__.py +0 -0
  91. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_azure_devops/oauth.py +0 -0
  92. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_azure_devops/provider.py +0 -0
  93. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_azure_devops/test.py +0 -0
  94. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_jira/__init__.py +0 -0
  95. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_jira/provider.py +0 -0
  96. {codee_agent-0.7.1 → codee_agent-0.7.3}/src/codee_tasks_jira/test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codee-agent
3
- Version: 0.7.1
3
+ Version: 0.7.3
4
4
  Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
5
5
  Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,codex,opencode,automation
6
6
  Author: Denis Kibalko
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codee-agent"
3
- version = "0.7.1"
3
+ version = "0.7.3"
4
4
  description = "A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -319,6 +319,7 @@ class AdminState(rx.State):
319
319
  adding_repository: bool = False
320
320
 
321
321
  active_jobs: list[ActiveJob] = []
322
+ paused: bool = False
322
323
  total_runs: int = 0
323
324
  last_24h_runs: int = 0
324
325
  hourly_runs: list[dict[str, Any]] = []
@@ -779,10 +780,14 @@ class AdminState(rx.State):
779
780
  def _refresh_dashboard(self) -> None:
780
781
  dashboard = SERVICE.dashboard()
781
782
  self.active_jobs = [_active_job(job) for job in dashboard["active"]]
783
+ self.paused = dashboard["paused"]
782
784
  self.total_runs = dashboard["counts"]["total"]
783
785
  self.last_24h_runs = dashboard["counts"]["last_24h"]
784
786
  self.hourly_runs = dashboard["hourly"]
785
787
 
788
+ def toggle_pause(self) -> None:
789
+ self.paused = SERVICE.set_paused(not self.paused)
790
+
786
791
  @rx.event(background=True)
787
792
  async def poll_dashboard(self) -> None:
788
793
  """Keep the dashboard live for as long as it is the page on screen.
@@ -1978,6 +1983,16 @@ def running_panel() -> rx.Component:
1978
1983
  font_family=MONO, class_name="codee-breathe",
1979
1984
  animation="codee-breathe 2.4s ease-in-out infinite"),
1980
1985
  ),
1986
+ rx.spacer(),
1987
+ rx.button(
1988
+ rx.cond(AdminState.paused,
1989
+ rx.icon("play", size=15),
1990
+ rx.icon("pause", size=15)),
1991
+ rx.cond(AdminState.paused, "Unpause", "Pause"),
1992
+ variant="outline",
1993
+ color_scheme=rx.cond(AdminState.paused, "green", "gray"),
1994
+ on_click=AdminState.toggle_pause,
1995
+ ),
1981
1996
  spacing="3",
1982
1997
  align="center",
1983
1998
  width="100%",
@@ -2136,52 +2151,50 @@ def dashboard_page() -> rx.Component:
2136
2151
 
2137
2152
 
2138
2153
  def skill_card(skill: SkillSummary) -> rx.Component:
2139
- return rx.box(
2140
- rx.vstack(
2141
- rx.hstack(rx.heading(skill.name, size="4"), rx.spacer(),
2142
- rx.badge(skill.type, color_scheme="blue", variant="soft"), width="100%"),
2143
- rx.text(skill.description, color=MUTED, font_size="0.9rem", min_height="2.7rem",
2144
- overflow="hidden"),
2154
+ return rx.vstack(
2155
+ rx.hstack(rx.heading(skill.name, size="4", class_name="skill-card-title"),
2156
+ rx.badge(skill.type, color_scheme="blue", variant="soft"),
2157
+ class_name="skill-card-head", width="100%"),
2158
+ rx.text(skill.description, color=MUTED, font_size="0.9rem",
2159
+ title=skill.description, class_name="skill-card-description"),
2160
+ rx.hstack(
2161
+ rx.icon("bot", size=14, color=SUBTLE_ICON),
2162
+ rx.text(skill.agent, color=MUTED, font_size="0.82rem"),
2163
+ rx.cond(skill.model != "",
2164
+ rx.code(skill.model, font_size="0.72rem",
2165
+ color_scheme="gray")),
2166
+ spacing="2", align="center", width="100%",
2167
+ class_name="skill-card-meta"),
2168
+ rx.cond(
2169
+ skill.issue_status != "",
2145
2170
  rx.hstack(
2146
- rx.icon("bot", size=14, color=SUBTLE_ICON),
2147
- rx.text(skill.agent, color=MUTED, font_size="0.82rem"),
2148
- rx.cond(skill.model != "",
2149
- rx.code(skill.model, font_size="0.72rem",
2150
- color_scheme="gray")),
2151
- spacing="2", align="center", width="100%"),
2152
- rx.cond(
2153
- skill.issue_status != "",
2154
- rx.hstack(
2155
- rx.badge(skill.issue_type, color_scheme="blue",
2156
- variant="outline"),
2157
- rx.icon("circle-dot", size=14, color=SUBTLE_ICON),
2158
- rx.text(skill.issue_status, color=MUTED,
2159
- font_size="0.82rem"),
2160
- spacing="2",
2161
- align="center",
2162
- width="100%",
2163
- background=HOVER,
2164
- padding="0.55rem 0.65rem",
2165
- border_radius="4px",
2166
- ),
2171
+ rx.badge(skill.issue_type, color_scheme="blue",
2172
+ variant="outline"),
2173
+ rx.icon("circle-dot", size=14, color=SUBTLE_ICON),
2174
+ rx.text(skill.issue_status, color=MUTED,
2175
+ font_size="0.82rem", title=skill.issue_status),
2176
+ spacing="2", align="center", width="100%",
2177
+ background=HOVER, padding="0.55rem 0.65rem",
2178
+ border_radius="4px", class_name="skill-card-trigger",
2167
2179
  ),
2168
- rx.button(rx.icon("pencil", size=15), "Edit", variant="soft",
2169
- on_click=AdminState.edit_skill(skill.slug), width="100%"),
2170
- spacing="4", align="start", height="100%", width="100%"),
2171
- padding="1rem", background=SURFACE, border=BORDER, min_height="185px"),
2180
+ ),
2181
+ rx.button(rx.icon("pencil", size=15), "Edit", variant="soft",
2182
+ on_click=AdminState.edit_skill(skill.slug), width="100%",
2183
+ class_name="skill-card-action"),
2184
+ class_name="skill-card", spacing="3", align="start", width="100%")
2172
2185
 
2173
2186
 
2174
2187
  def agents_card() -> rx.Component:
2175
- return rx.box(
2176
- rx.vstack(
2177
- rx.hstack(rx.heading(AGENTS_FILE, size="4"), rx.spacer(),
2178
- rx.badge("always on", color_scheme="gray", variant="soft"), width="100%"),
2179
- rx.text("Plain-text instructions every coding-agent run loads. Cannot be deleted.",
2180
- color=MUTED, font_size="0.9rem", min_height="2.7rem", overflow="hidden"),
2181
- rx.button(rx.icon("pencil", size=15), "Edit", variant="soft",
2182
- on_click=AdminState.edit_agents, width="100%"),
2183
- spacing="4", align="start", height="100%", width="100%"),
2184
- padding="1rem", background=SURFACE, border=BORDER, min_height="185px")
2188
+ return rx.vstack(
2189
+ rx.hstack(rx.heading(AGENTS_FILE, size="4", class_name="skill-card-title"),
2190
+ rx.badge("always on", color_scheme="gray", variant="soft"),
2191
+ class_name="skill-card-head", width="100%"),
2192
+ rx.text("Plain-text instructions every coding-agent run loads. Cannot be deleted.",
2193
+ color=MUTED, font_size="0.9rem", class_name="skill-card-description"),
2194
+ rx.button(rx.icon("pencil", size=15), "Edit", variant="soft",
2195
+ on_click=AdminState.edit_agents, width="100%",
2196
+ class_name="skill-card-action"),
2197
+ class_name="skill-card", spacing="3", align="start", width="100%")
2185
2198
 
2186
2199
 
2187
2200
  def agents_editor() -> rx.Component:
@@ -3693,6 +3706,43 @@ app = rx.App(
3693
3706
  "button:not(:disabled), [role='button']:not([aria-disabled='true'])": {
3694
3707
  "cursor": "pointer",
3695
3708
  },
3709
+ ".skill-card": {
3710
+ "min_height": "17rem",
3711
+ "height": "100%",
3712
+ "min_width": "0",
3713
+ "padding": "1rem",
3714
+ "background": SURFACE,
3715
+ "border": BORDER,
3716
+ "transition": "border-color 0.15s ease, box-shadow 0.15s ease",
3717
+ },
3718
+ ".skill-card:hover": {
3719
+ "border_color": ACCENT,
3720
+ "box_shadow": "0 4px 16px rgba(24, 39, 75, 0.08)",
3721
+ },
3722
+ ".skill-card-head": {"align_items": "flex-start", "min_width": "0"},
3723
+ ".skill-card-head > :last-child": {"flex_shrink": "0"},
3724
+ ".skill-card-title": {
3725
+ "min_width": "0",
3726
+ "flex": "1",
3727
+ "overflow_wrap": "anywhere",
3728
+ "display": "-webkit-box",
3729
+ "-webkit-box-orient": "vertical",
3730
+ "-webkit-line-clamp": "2",
3731
+ "overflow": "hidden",
3732
+ },
3733
+ ".skill-card-description": {
3734
+ "line_height": "1.45",
3735
+ "display": "-webkit-box",
3736
+ "-webkit-box-orient": "vertical",
3737
+ "-webkit-line-clamp": "3",
3738
+ "overflow": "hidden",
3739
+ },
3740
+ ".skill-card-meta, .skill-card-trigger": {"min_width": "0"},
3741
+ ".skill-card-trigger > :last-child": {
3742
+ "overflow": "hidden", "text_overflow": "ellipsis",
3743
+ "white_space": "nowrap", "min_width": "0",
3744
+ },
3745
+ ".skill-card-action": {"margin_top": "auto"},
3696
3746
  # Expanding halo behind the live dot on in-flight runs.
3697
3747
  "@keyframes codee-ping": {
3698
3748
  "0%": {"transform": "scale(1)", "opacity": "0.55"},
@@ -35,6 +35,7 @@ from codee.lib import runs_db
35
35
  from codee.lib.claude_key_rotation import ensure_fresh
36
36
  from codee.lib.cron_describe import describe_cron
37
37
  from codee.lib.mcp_config import find_mcp_server, write_mcp_server
38
+ from codee.lib.runtime_control import is_paused, set_paused
38
39
  from codee.lib.trigger_cron_skills import request_force_run
39
40
  from codee.lib.trigger_issue_skills import (
40
41
  IssueTriggeredSkill,
@@ -2049,8 +2050,14 @@ class AdminService:
2049
2050
  "active": active,
2050
2051
  "counts": runs_db.counts(main_context=self.context),
2051
2052
  "hourly": runs_db.runs_by_hour(main_context=self.context),
2053
+ "paused": is_paused(self.context),
2052
2054
  }
2053
2055
 
2056
+ def set_paused(self, paused: bool) -> bool:
2057
+ """Allow or prevent future automated agent runs."""
2058
+ set_paused(self.context, paused)
2059
+ return is_paused(self.context)
2060
+
2054
2061
  def _task_url(self) -> Callable[[str], str]:
2055
2062
  """How to link a work item key, per the configured tasks provider.
2056
2063
 
@@ -21,6 +21,7 @@ from codee_tasks_abstract.provider import AbstractTasksProvider
21
21
 
22
22
  from codee.coding_agents import resolve_agent_code
23
23
  from codee.lib import claude_key_rotation, runs_db
24
+ from codee.lib.runtime_control import is_paused
24
25
  from codee.lib.trigger_aws_sqs_skills import trigger_aws_sqs_skills
25
26
  from codee.lib.trigger_cron_skills import trigger_cron_skills
26
27
  from codee.lib.trigger_email_skills import trigger_email_skills
@@ -359,6 +360,9 @@ def _submit_task(task_id: str, message: str, session_id: str, skill_name: str,
359
360
  against the next tick.
360
361
  """
361
362
  with _inflight_lock:
363
+ if is_paused(context):
364
+ log.debug("New agent work was paused before %s could start.", task_id)
365
+ return False
362
366
  if task_id in _inflight:
363
367
  log.debug("%s already running; skipping duplicate launch.", task_id)
364
368
  return False
@@ -387,6 +391,10 @@ def run_once() -> None:
387
391
  if not _pull_latest_code():
388
392
  log.warning("Failed to pull from the repo, still continuing...")
389
393
 
394
+ if is_paused(context):
395
+ log.info("New agent work is paused; active agents continue running.")
396
+ return
397
+
390
398
  trigger_cron_skills(_run_agent, main_context=context)
391
399
  trigger_aws_sqs_skills(_run_agent, main_context=context)
392
400
  trigger_email_skills(_run_agent, main_context=context)
@@ -0,0 +1,21 @@
1
+ """Persistent controls shared by the admin UI and executor process."""
2
+ from pathlib import Path
3
+
4
+ from codee_main_context.context import CodeeMainContext
5
+
6
+ PAUSED_FILE = "paused"
7
+
8
+
9
+ def is_paused(context: CodeeMainContext) -> bool:
10
+ """Return whether new agent work is paused."""
11
+ return (context.data_dir / PAUSED_FILE).exists()
12
+
13
+
14
+ def set_paused(context: CodeeMainContext, paused: bool) -> None:
15
+ """Persist whether new agent work may start."""
16
+ path = context.data_dir / PAUSED_FILE
17
+ if paused:
18
+ path.parent.mkdir(parents=True, exist_ok=True)
19
+ path.touch(exist_ok=True)
20
+ else:
21
+ path.unlink(missing_ok=True)
@@ -62,6 +62,17 @@ def _write_issue_skill(root: Path) -> Path:
62
62
 
63
63
 
64
64
  class RecentRunsTest(unittest.TestCase):
65
+ def test_dashboard_pause_state_persists(self) -> None:
66
+ with tempfile.TemporaryDirectory() as directory:
67
+ service = AdminService.__new__(AdminService)
68
+ service.data_dir = Path(directory)
69
+ service.context = CodeeMainContext(data_dir=service.data_dir)
70
+
71
+ self.assertFalse(service.set_paused(False))
72
+ self.assertTrue(service.set_paused(True))
73
+ self.assertTrue(service.dashboard()["paused"])
74
+ self.assertFalse(service.set_paused(False))
75
+
65
76
  def test_formats_relative_age_in_minutes_hours_and_days(self) -> None:
66
77
  now = datetime(2026, 9, 24, 12, 0, tzinfo=timezone.utc)
67
78
  cases = [
@@ -533,6 +533,27 @@ class ParentWorkItemSkipTest(unittest.TestCase):
533
533
  executor.run_once()
534
534
  return [call.args[0] for call in submit.call_args_list]
535
535
 
536
+ def test_paused_executor_starts_no_trigger_or_issue_work(self) -> None:
537
+ with patch.object(executor, "is_paused", return_value=True), \
538
+ patch.object(executor, "trigger_cron_skills") as cron, \
539
+ patch.object(executor, "trigger_aws_sqs_skills") as sqs, \
540
+ patch.object(executor, "trigger_email_skills") as email:
541
+ executor.run_once()
542
+
543
+ cron.assert_not_called()
544
+ sqs.assert_not_called()
545
+ email.assert_not_called()
546
+ self.provider.get_tasks.assert_not_called()
547
+
548
+ def test_pause_while_polling_prevents_issue_launch(self) -> None:
549
+ with patch.object(executor, "is_paused", return_value=True), \
550
+ patch.object(executor, "_run_task") as run_task:
551
+ launched = executor._submit_task(
552
+ "NIM-4", "prompt", "session", "Task developer")
553
+
554
+ self.assertFalse(launched)
555
+ run_task.assert_not_called()
556
+
536
557
  def test_an_item_under_a_mapped_parent_is_left_to_that_parents_run(self) -> None:
537
558
  self.assertEqual(
538
559
  self._submitted(_polled_task("NIM-1", parent_type="Story")), [])
File without changes
File without changes