From 649d218fdf402133dac275aabec276daa309f70c Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Tue, 9 Jun 2026 23:10:38 +0200 Subject: [PATCH 1/2] Expose task scheduled / deadline through the CLI task add / update commands and surface the dates in task list and the MCP list_tasks description Backend, API, MCP add_task/update_task, and the React UI already wrote these fields; the CLI was the missing seam so neither a shell user nor a script could set or change a snooze / deadline from the terminal. Three small wiring changes: - task add gains --scheduled / --deadline options, both accepting YYYY-MM-DD or omission for no value. - task update gains the same two options. The empty-string pattern from the rest of update_task still applies: '' clears. - _format_task_line appends scheduled:DATE / deadline:DATE segments when present so the dates show up in plain task list output without forcing --json. - list_tasks MCP description now mentions both fields so the advisor knows they are part of the returned shape (the data already crossed the wire; this just lights it up in the schema description). --- kaisho/cli/task.py | 26 ++++++++++++++++++++++++-- kaisho/cron/tool_defs.py | 6 ++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/kaisho/cli/task.py b/kaisho/cli/task.py index 7b90b178..af687b87 100644 --- a/kaisho/cli/task.py +++ b/kaisho/cli/task.py @@ -24,6 +24,12 @@ def _format_task_line(task: dict) -> str: if tag_str: parts.append(tag_str) parts.append(created) + scheduled = task.get("scheduled") + deadline = task.get("deadline") + if scheduled: + parts.append(f"scheduled:{scheduled}") + if deadline: + parts.append(f"deadline:{deadline}") return " ".join(parts) @@ -41,10 +47,16 @@ def task(): help="Task body/description") @click.option("--github-url", default=None, help="GitHub issue/PR URL") +@click.option("--scheduled", default=None, + help="Snooze date (YYYY-MM-DD). " + "Task hidden until that day arrives.") +@click.option("--deadline", default=None, + help="Deadline date (YYYY-MM-DD).") @click.option("--json", "as_json", is_flag=True, help="JSON output") def task_add(customer_name, title, tags, status, - body, github_url, as_json): + body, github_url, scheduled, deadline, + as_json): """Add a new task.""" backend = get_backend() backend.customers.ensure_customer(customer_name or "") @@ -55,6 +67,8 @@ def task_add(customer_name, title, tags, status, tags=list(tags), body=body, github_url=github_url, + scheduled=scheduled, + deadline=deadline, ) if as_json: click.echo(json.dumps(result, default=str)) @@ -187,9 +201,15 @@ def task_show(task_id, as_json): help="New body text") @click.option("--github-url", default=None, help="GitHub issue/PR URL") +@click.option("--scheduled", default=None, + help="Snooze date (YYYY-MM-DD). " + "Pass empty string to clear.") +@click.option("--deadline", default=None, + help="Deadline date (YYYY-MM-DD). " + "Pass empty string to clear.") @click.option("--json", "as_json", is_flag=True) def task_update(task_id, title, customer, body, - github_url, as_json): + github_url, scheduled, deadline, as_json): """Update a task's fields.""" result = get_backend().tasks.update_task( task_id=task_id, @@ -197,6 +217,8 @@ def task_update(task_id, title, customer, body, customer=customer, body=body, github_url=github_url, + scheduled=scheduled, + deadline=deadline, ) if as_json: click.echo(json.dumps(result, default=str)) diff --git a/kaisho/cron/tool_defs.py b/kaisho/cron/tool_defs.py index 56d80260..df4f5c44 100644 --- a/kaisho/cron/tool_defs.py +++ b/kaisho/cron/tool_defs.py @@ -19,8 +19,10 @@ "name": "list_tasks", "tier": "read", "description": ( - "List tasks from the kanban board. " - "Returns id, customer, title, status, tags for each task." + "List tasks from the kanban board. Returns " + "id, customer, title, status, tags, scheduled " + "(snooze date, YYYY-MM-DD or null), and " + "deadline (YYYY-MM-DD or null) for each task." ), "input_schema": { "type": "object", From fd02bc9eb5ac909ba360a7c5b751d8c7b384bacf Mon Sep 17 00:00:00 2001 From: Ramon Bartl Date: Tue, 9 Jun 2026 23:24:00 +0200 Subject: [PATCH 2/2] Reject deadline-before-scheduled at the API and gate Save in the edit form so the typo can't sneak in A snoozed-past-its-deadline task is incoherent: the deadline badge would fire before the snooze even surfaces. Almost certainly a typo, but the four layers (backend, API, CLI, frontend) all accepted any combination of dates without checking. API: TaskCreate and TaskUpdate both gain a model_validator that rejects with 422 when both fields are present in the same payload AND deadline < scheduled. Lex compare on YYYY-MM-DD agrees with chronological order, so a plain string compare is enough. The PATCH check only catches the 'user sent both at once' case; sending one that breaks the invariant against the stored other is not validated server-side (the frontend has the merged shape and gates that locally). Frontend: TaskEditForm computes datesOutOfOrder from the two edit-state strings and shows a red inline message plus disables the Save button when the invariant is violated. Cmd+Enter and the Save click both short-circuit through a handleSave gate so accidental commits are impossible from the form. i18n key added in all four locales (en/de/es/ru). Tests: three new API cases in TestKanban -- create rejects, same-day passes, update rejects -- and the existing 699 backend tests still pass. --- .../src/components/kanban/TaskEditForm.tsx | 28 ++++++++- frontend/src/locales/de/kanban.json | 1 + frontend/src/locales/en/kanban.json | 1 + frontend/src/locales/es/kanban.json | 1 + frontend/src/locales/ru/kanban.json | 1 + kaisho/api/routers/kanban.py | 46 ++++++++++++++- tests/test_api.py | 59 +++++++++++++++++++ 7 files changed, 133 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/kanban/TaskEditForm.tsx b/frontend/src/components/kanban/TaskEditForm.tsx index 91226ada..595b5237 100644 --- a/frontend/src/components/kanban/TaskEditForm.tsx +++ b/frontend/src/components/kanban/TaskEditForm.tsx @@ -71,13 +71,30 @@ export function TaskEditForm({ const { t } = useTranslation("kanban"); const { t: tc } = useTranslation("common"); + // Cross-field check: a snooze date past the deadline + // is incoherent (the deadline badge would fire before + // the snooze even surfaces). Lex compare agrees with + // chronological order on ``YYYY-MM-DD``. The API + // mirrors this rule and returns 400 if it slips + // through, so this is purely instant-feedback UX. + const datesOutOfOrder = !!( + editScheduled + && editDeadline + && editDeadline < editScheduled + ); + + function handleSave() { + if (datesOutOfOrder) return; + onSave(); + } + function handleKeyDown(e: React.KeyboardEvent) { if ( (e.metaKey || e.ctrlKey) && e.key === "Enter" ) { e.preventDefault(); - onSave(); + handleSave(); } if (e.key === "Escape") { onCancel(); @@ -155,6 +172,11 @@ export function TaskEditForm({ /> + {datesOutOfOrder && ( +

+ {t("datesOutOfOrder")} +

+ )}
e.stopPropagation()} > @@ -177,8 +199,8 @@ export function TaskEditForm({