Skip to content

Commit fef0959

Browse files
committed
Reset tasks clears estimate
1 parent aab2f40 commit fef0959

5 files changed

Lines changed: 95 additions & 5 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "codexapi"
7-
version = "0.5.15"
7+
version = "0.5.16"
88
description = "Minimal Python API for running the Codex CLI."
99
readme = "README.md"
1010
requires-python = ">=3.8"
@@ -17,7 +17,7 @@ classifiers = [
1717

1818
dependencies = [
1919
"PyYAML>=6.0",
20-
"gh-task",
20+
"gh-task>=0.1.7",
2121
"tqdm>=4.64",
2222
]
2323

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
PyYAML>=6.0
22
tqdm>=4.64
3-
gh-task
3+
gh-task>=0.1.7

src/codexapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
"task",
1616
"task_result",
1717
]
18-
__version__ = "0.5.15"
18+
__version__ = "0.5.16"

src/codexapi/cli.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,6 +1291,29 @@ def main(argv=None):
12911291
help="Filename for the new task file.",
12921292
)
12931293

1294+
reset_parser = subparsers.add_parser(
1295+
"reset",
1296+
help="Reset project tasks back to Ready.",
1297+
)
1298+
reset_parser.add_argument(
1299+
"-p",
1300+
"--project",
1301+
required=True,
1302+
help="GitHub Project ref (owner/projects/3).",
1303+
)
1304+
reset_parser.add_argument(
1305+
"-n",
1306+
"--name",
1307+
default="reset",
1308+
help="Owner label name for gh-task (default: reset).",
1309+
)
1310+
reset_parser.add_argument(
1311+
"-d",
1312+
"--description",
1313+
action="store_true",
1314+
help="Remove any Progress section in the issue body.",
1315+
)
1316+
12941317
subparsers.add_parser(
12951318
"top",
12961319
help="Show running Codex sessions.",
@@ -1303,6 +1326,15 @@ def main(argv=None):
13031326
if args.command == "create":
13041327
_create_task_template(args.filename)
13051328
return
1329+
if args.command == "reset":
1330+
from .gh_integration import reset_project_tasks
1331+
1332+
issues = reset_project_tasks(args.project, args.name, args.description)
1333+
for issue in issues:
1334+
title = (issue.title or "Untitled issue").strip()
1335+
print(f"{issue.repo}#{issue.number} {title}")
1336+
print(f"Reset {len(issues)} task(s).")
1337+
return
13061338
if args.command == "top":
13071339
_run_top([])
13081340
return

src/codexapi/gh_integration.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from tqdm import tqdm
77

8-
from gh_task.project import Project
8+
from gh_task.project import Project, UPDATE_STATUS_MUTATION
99

1010
from .taskfile import TaskFile
1111

@@ -17,6 +17,7 @@
1717
_FAILURE_LABEL = "⨉"
1818
_SUCCESS_COLOR = "2da44e"
1919
_FAILURE_COLOR = "d73a4a"
20+
_OWNER_PREFIX = "owner:"
2021

2122

2223
def _canonical_task_name(path):
@@ -42,6 +43,63 @@ def project_url(project):
4243
return f"https://github.com/{owner}/projects/{number}"
4344

4445

46+
def reset_project_tasks(project, name, description=False):
47+
"""Reset owned issues in a project back to Ready."""
48+
project = Project(project, name)
49+
owner_projects = {}
50+
issues = []
51+
for status in project.statuses():
52+
for issue in project.list(status, return_issue=True):
53+
issue = project.get_issue(issue, require_project_item=True)
54+
labels = issue.labels or []
55+
owner_labels = [label for label in labels if label.lower().startswith(_OWNER_PREFIX)]
56+
if not owner_labels:
57+
continue
58+
issues.append((issue, owner_labels))
59+
60+
ready_name, ready_option = project._resolve_status("Ready")
61+
project._ensure_project_loaded()
62+
try:
63+
project._resolve_number_field("Estimate")
64+
estimate_supported = True
65+
except Exception:
66+
estimate_supported = False
67+
68+
for issue, owner_labels in issues:
69+
owner_name = None
70+
for label in owner_labels:
71+
parts = label.split(":", 1)
72+
if len(parts) == 2 and parts[1].strip():
73+
owner_name = parts[1].strip()
74+
break
75+
if owner_name and estimate_supported:
76+
owner_project = owner_projects.get(owner_name)
77+
if owner_project is None:
78+
owner_project = Project(project.owner + "/projects/" + str(project.number), owner_name)
79+
owner_projects[owner_name] = owner_project
80+
owner_project.set_estimate(issue, None)
81+
for label in owner_labels:
82+
project._remove_label(issue, label)
83+
project._remove_label(issue, _SUCCESS_LABEL)
84+
project._remove_label(issue, _FAILURE_LABEL)
85+
if (issue.status or "").lower() != ready_name.lower():
86+
project.client.graphql(
87+
UPDATE_STATUS_MUTATION,
88+
{
89+
"projectId": project._project_id,
90+
"itemId": issue.project_item_id,
91+
"fieldId": project._status_field_id,
92+
"optionId": ready_option,
93+
},
94+
)
95+
if description:
96+
body = issue.body if issue.body is not None else project.get_issue_body(issue)
97+
cleaned = _strip_progress_section(body)
98+
if cleaned != body:
99+
project.set_issue_body(issue, cleaned)
100+
return [issue for issue, _labels in issues]
101+
102+
45103
def _task_file_map(task_files):
46104
mapping = {}
47105
for path in task_files:

0 commit comments

Comments
 (0)