Skip to content

Commit 943ecff

Browse files
committed
release: bump version to 0.4.7rc1
1 parent b2fa843 commit 943ecff

6 files changed

Lines changed: 140 additions & 7 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "cccc-pair"
7-
version = "0.4.6"
7+
version = "0.4.7rc1"
88
description = "Global multi-agent delivery kernel with working groups, scopes, and an append-only collaboration ledger"
99
readme = { file = "README.md", content-type = "text/markdown" }
1010
requires-python = ">=3.9"

src/cccc/ports/mcp/handlers/context.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ def task_update(
177177
task_id: str,
178178
title: Optional[str] = None,
179179
outcome: Optional[str] = None,
180+
status: Optional[str] = None,
180181
parent_id: Optional[str] = None,
181182
assignee: Optional[str] = None,
182183
priority: Optional[str] = None,
@@ -188,27 +189,44 @@ def task_update(
188189
by: Optional[str] = None,
189190
) -> Dict[str, Any]:
190191
op: Dict[str, Any] = {"op": "task.update", "task_id": task_id}
192+
has_patch_fields = False
191193
if title is not None:
192194
op["title"] = title
195+
has_patch_fields = True
193196
if outcome is not None:
194197
op["outcome"] = outcome
198+
has_patch_fields = True
195199
if parent_id is not None:
196200
op["parent_id"] = parent_id
201+
has_patch_fields = True
197202
if assignee is not None:
198203
op["assignee"] = assignee
204+
has_patch_fields = True
199205
if priority is not None:
200206
op["priority"] = priority
207+
has_patch_fields = True
201208
if blocked_by is not None:
202209
op["blocked_by"] = blocked_by
210+
has_patch_fields = True
203211
if waiting_on is not None:
204212
op["waiting_on"] = waiting_on
213+
has_patch_fields = True
205214
if handoff_to is not None:
206215
op["handoff_to"] = handoff_to
216+
has_patch_fields = True
207217
if notes is not None:
208218
op["notes"] = notes
219+
has_patch_fields = True
209220
if checklist is not None:
210221
op["checklist"] = checklist
211-
return context_sync(group_id=group_id, ops=[op], by=by)
222+
has_patch_fields = True
223+
224+
ops: List[Dict[str, Any]] = []
225+
if has_patch_fields or status is None:
226+
ops.append(op)
227+
if status is not None:
228+
ops.append({"op": "task.move", "task_id": task_id, "status": status})
229+
return context_sync(group_id=group_id, ops=ops, by=by)
212230

213231

214232
def task_move(*, group_id: str, task_id: str, status: str, by: Optional[str] = None) -> Dict[str, Any]:
@@ -584,7 +602,7 @@ def _handle_context_namespace(
584602
"task_id": str(arguments.get("task_id") or ""),
585603
"by": by,
586604
}
587-
for field in ("title", "outcome", "parent_id", "assignee", "priority", "waiting_on", "handoff_to", "notes"):
605+
for field in ("title", "outcome", "status", "parent_id", "assignee", "priority", "waiting_on", "handoff_to", "notes"):
588606
if field in arguments:
589607
value = arguments.get(field)
590608
kwargs[field] = str(value) if value is not None else None

src/cccc/ports/mcp/toolspecs.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ def _obj(properties: dict, required: list[str] | None = None) -> dict:
575575
},
576576
{
577577
"name": "cccc_task",
578-
"description": "Shared collaboration task hub (not runtime todo): action=list|create|update|move|restore|delete. Use for multi-actor, long-horizon, or user-tracked work.",
578+
"description": "Shared collaboration task hub (not runtime todo): action=list|create|update|move|restore|delete. Use for multi-actor, long-horizon, or user-tracked work. Lifecycle transitions are canonical via move; update with status auto-applies the matching lifecycle move.",
579579
"inputSchema": _obj(
580580
{
581581
**_COMMON_GROUP,
@@ -589,7 +589,11 @@ def _obj(properties: dict, required: list[str] | None = None) -> dict:
589589
"include_archived": {"type": "boolean", "default": False},
590590
"title": {"type": "string"},
591591
"outcome": {"type": "string"},
592-
"status": {"type": "string", "enum": ["planned", "active", "done", "archived"]},
592+
"status": {
593+
"type": "string",
594+
"enum": ["planned", "active", "done", "archived"],
595+
"description": "Lifecycle status. Required for action=move. If passed with action=update, the wrapper also applies the corresponding lifecycle transition.",
596+
},
593597
"parent_id": {"type": "string"},
594598
"assignee": {"type": "string"},
595599
"priority": {"type": "string"},

src/cccc/resources/cccc-help.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ This user is not generic. Learn their bar and dislikes; let that shape your defa
6161
- Update the brief with `cccc_coordination(action="update_brief"|...)`.
6262
- Add decisions and handoffs with `cccc_coordination(action="add_decision"|"add_handoff", ...)`.
6363
- Use `cccc_task` for shared work units; runtime todo stays private.
64+
- For task lifecycle changes, use `cccc_task(action="move", ...)` as the canonical path. `update` is for task fields; if `status` is included with `update`, the MCP wrapper also applies the matching move.
6465

6566
### Agent State
6667

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import unittest
2+
from unittest.mock import patch
3+
4+
5+
class TestMcpTaskUpdateStatusWrapper(unittest.TestCase):
6+
def test_task_update_with_status_batches_update_and_move(self) -> None:
7+
from cccc.ports.mcp import server as mcp_server
8+
from cccc.ports.mcp.handlers import context as mcp_context
9+
10+
captured = {}
11+
12+
def _fake_context_sync(*, group_id, ops, dry_run=False, if_version=None, by=None):
13+
captured["group_id"] = group_id
14+
captured["ops"] = ops
15+
captured["dry_run"] = dry_run
16+
captured["if_version"] = if_version
17+
captured["by"] = by
18+
return {"ok": True}
19+
20+
with patch.object(mcp_server, "_resolve_group_id", return_value="g_test"), patch.object(
21+
mcp_server, "_resolve_self_actor_id", return_value="peer1"
22+
), patch.object(mcp_context, "context_sync", side_effect=_fake_context_sync):
23+
out = mcp_server.handle_tool_call(
24+
"cccc_task",
25+
{
26+
"action": "update",
27+
"task_id": "T123",
28+
"title": "Ship the patch",
29+
"status": "done",
30+
},
31+
)
32+
33+
self.assertTrue(bool(out.get("ok")))
34+
self.assertEqual(captured.get("group_id"), "g_test")
35+
self.assertEqual(captured.get("by"), "peer1")
36+
self.assertEqual(
37+
captured.get("ops"),
38+
[
39+
{"op": "task.update", "task_id": "T123", "title": "Ship the patch"},
40+
{"op": "task.move", "task_id": "T123", "status": "done"},
41+
],
42+
)
43+
44+
def test_task_update_with_status_only_degenerates_to_move(self) -> None:
45+
from cccc.ports.mcp import server as mcp_server
46+
from cccc.ports.mcp.handlers import context as mcp_context
47+
48+
captured = {}
49+
50+
def _fake_context_sync(*, group_id, ops, dry_run=False, if_version=None, by=None):
51+
captured["group_id"] = group_id
52+
captured["ops"] = ops
53+
captured["by"] = by
54+
return {"ok": True}
55+
56+
with patch.object(mcp_server, "_resolve_group_id", return_value="g_test"), patch.object(
57+
mcp_server, "_resolve_self_actor_id", return_value="peer1"
58+
), patch.object(mcp_context, "context_sync", side_effect=_fake_context_sync):
59+
out = mcp_server.handle_tool_call(
60+
"cccc_task",
61+
{
62+
"action": "update",
63+
"task_id": "T123",
64+
"status": "active",
65+
},
66+
)
67+
68+
self.assertTrue(bool(out.get("ok")))
69+
self.assertEqual(captured.get("group_id"), "g_test")
70+
self.assertEqual(captured.get("by"), "peer1")
71+
self.assertEqual(
72+
captured.get("ops"),
73+
[{"op": "task.move", "task_id": "T123", "status": "active"}],
74+
)
75+
76+
def test_task_update_without_status_stays_plain_patch(self) -> None:
77+
from cccc.ports.mcp import server as mcp_server
78+
from cccc.ports.mcp.handlers import context as mcp_context
79+
80+
captured = {}
81+
82+
def _fake_context_sync(*, group_id, ops, dry_run=False, if_version=None, by=None):
83+
captured["group_id"] = group_id
84+
captured["ops"] = ops
85+
captured["by"] = by
86+
return {"ok": True}
87+
88+
with patch.object(mcp_server, "_resolve_group_id", return_value="g_test"), patch.object(
89+
mcp_server, "_resolve_self_actor_id", return_value="peer1"
90+
), patch.object(mcp_context, "context_sync", side_effect=_fake_context_sync):
91+
out = mcp_server.handle_tool_call(
92+
"cccc_task",
93+
{
94+
"action": "update",
95+
"task_id": "T123",
96+
"notes": "Need one more repro step.",
97+
},
98+
)
99+
100+
self.assertTrue(bool(out.get("ok")))
101+
self.assertEqual(captured.get("group_id"), "g_test")
102+
self.assertEqual(captured.get("by"), "peer1")
103+
self.assertEqual(
104+
captured.get("ops"),
105+
[{"op": "task.update", "task_id": "T123", "notes": "Need one more repro step."}],
106+
)
107+
108+
109+
if __name__ == "__main__":
110+
unittest.main()

web/src/stores/useUIStore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { create } from "zustand";
33

44
export const SIDEBAR_COLLAPSED_WIDTH = 60;
55
export const SIDEBAR_DEFAULT_WIDTH = 280;
6-
export const SIDEBAR_MIN_WIDTH = 240;
7-
export const SIDEBAR_MAX_WIDTH = 420;
6+
export const SIDEBAR_MIN_WIDTH = 280;
7+
export const SIDEBAR_MAX_WIDTH = 480;
88

99
interface UINotice {
1010
message: string;

0 commit comments

Comments
 (0)