project-agent 0.2.1__tar.gz → 0.2.2__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 (159) hide show
  1. {project_agent-0.2.1 → project_agent-0.2.2}/PKG-INFO +1 -1
  2. {project_agent-0.2.1 → project_agent-0.2.2}/pyproject.toml +1 -1
  3. project_agent-0.2.2/src/project_agent/application/context/project_summary.py +144 -0
  4. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/project_context/models.py +26 -0
  5. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/db/repositories.py +4 -0
  6. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/context.py +71 -0
  7. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent.egg-info/PKG-INFO +1 -1
  8. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent.egg-info/SOURCES.txt +2 -0
  9. project_agent-0.2.2/tests/test_context_project_summary.py +322 -0
  10. {project_agent-0.2.1 → project_agent-0.2.2}/README.md +0 -0
  11. {project_agent-0.2.1 → project_agent-0.2.2}/setup.cfg +0 -0
  12. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/__init__.py +0 -0
  13. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/__init__.py +0 -0
  14. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/bootstrap/__init__.py +0 -0
  15. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/bootstrap/checkpoint.py +0 -0
  16. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/bootstrap/init.py +0 -0
  17. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/bootstrap/loop_state.py +0 -0
  18. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/__init__.py +0 -0
  19. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/binding_normalization.py +0 -0
  20. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/get_project_context.py +0 -0
  21. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/get_project_owner.py +0 -0
  22. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/get_task.py +0 -0
  23. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/get_user_context.py +0 -0
  24. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/list_projects.py +0 -0
  25. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/list_tasks.py +0 -0
  26. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/resolve_binding.py +0 -0
  27. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/resolve_group.py +0 -0
  28. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/resolve_user.py +0 -0
  29. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/search_user.py +0 -0
  30. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/context/upsert_user_identity.py +0 -0
  31. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/doc/__init__.py +0 -0
  32. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/doc/render.py +0 -0
  33. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/doc/sync.py +0 -0
  34. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/doc/writeback.py +0 -0
  35. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/doc_hub/__init__.py +0 -0
  36. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/doc_hub/manage.py +0 -0
  37. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/doc_hub/read.py +0 -0
  38. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/loop/__init__.py +0 -0
  39. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/loop/enqueue.py +0 -0
  40. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/loop/run.py +0 -0
  41. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/__init__.py +0 -0
  42. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/create_version.py +0 -0
  43. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/delete_task.py +0 -0
  44. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/finalize_version_closure.py +0 -0
  45. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/milestones.py +0 -0
  46. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/planned_versions.py +0 -0
  47. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/propose_change.py +0 -0
  48. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/read.py +0 -0
  49. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/set_tracking.py +0 -0
  50. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/signoff_proposal.py +0 -0
  51. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/snooze_task_reminder.py +0 -0
  52. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/switch_version.py +0 -0
  53. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/update_version.py +0 -0
  54. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/mind/write_task_update.py +0 -0
  55. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/monitoring/__init__.py +0 -0
  56. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/monitoring/evaluate_followups.py +0 -0
  57. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/monitoring/plan.py +0 -0
  58. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/monitoring/run.py +0 -0
  59. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/report/__init__.py +0 -0
  60. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/report/build.py +0 -0
  61. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/scheduled/__init__.py +0 -0
  62. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/scheduled/run.py +0 -0
  63. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/version_closure/__init__.py +0 -0
  64. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/application/version_closure/evaluate.py +0 -0
  65. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/__init__.py +0 -0
  66. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/bootstrap/__init__.py +0 -0
  67. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/bootstrap/models.py +0 -0
  68. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/doc/__init__.py +0 -0
  69. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/doc/models.py +0 -0
  70. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/doc_hub/__init__.py +0 -0
  71. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/doc_hub/models.py +0 -0
  72. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/loop/__init__.py +0 -0
  73. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/loop/models.py +0 -0
  74. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/mind/__init__.py +0 -0
  75. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/mind/models.py +0 -0
  76. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/monitoring/__init__.py +0 -0
  77. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/monitoring/evaluator.py +0 -0
  78. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/monitoring/models.py +0 -0
  79. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/project_context/__init__.py +0 -0
  80. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/report/__init__.py +0 -0
  81. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/report/models.py +0 -0
  82. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/scheduled/__init__.py +0 -0
  83. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/scheduled/models.py +0 -0
  84. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/shared/__init__.py +0 -0
  85. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/shared/documents.py +0 -0
  86. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/shared/models.py +0 -0
  87. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/shared/project_mind_derived.py +0 -0
  88. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/shared/project_mind_readiness.py +0 -0
  89. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/shared/statuses.py +0 -0
  90. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/user/__init__.py +0 -0
  91. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/user/models.py +0 -0
  92. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/version_closure/__init__.py +0 -0
  93. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/version_closure/evaluator.py +0 -0
  94. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/domain/version_closure/models.py +0 -0
  95. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/__init__.py +0 -0
  96. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/clock/__init__.py +0 -0
  97. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/clock/workday_clock.py +0 -0
  98. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/db/__init__.py +0 -0
  99. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/db/migration.py +0 -0
  100. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/db/schema.py +0 -0
  101. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/db/session.py +0 -0
  102. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/infrastructure/db/tables.py +0 -0
  103. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/__init__.py +0 -0
  104. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/__init__.py +0 -0
  105. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/app.py +0 -0
  106. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/__init__.py +0 -0
  107. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/bootstrap.py +0 -0
  108. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/db.py +0 -0
  109. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/doc.py +0 -0
  110. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/doc_hub.py +0 -0
  111. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/loop.py +0 -0
  112. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/mind.py +0 -0
  113. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/monitoring.py +0 -0
  114. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/project_boundary.py +0 -0
  115. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/report.py +0 -0
  116. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/scheduled.py +0 -0
  117. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/task_event_notify.py +0 -0
  118. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/upgrade.py +0 -0
  119. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/commands/version_closure.py +0 -0
  120. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent/interface/cli/helptext.py +0 -0
  121. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent.egg-info/dependency_links.txt +0 -0
  122. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent.egg-info/entry_points.txt +0 -0
  123. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent.egg-info/requires.txt +0 -0
  124. {project_agent-0.2.1 → project_agent-0.2.2}/src/project_agent.egg-info/top_level.txt +0 -0
  125. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_bootstrap_checkpoint.py +0 -0
  126. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_cli_help.py +0 -0
  127. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_cli_version.py +0 -0
  128. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_context_get_project_owner.py +0 -0
  129. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_context_get_project_pending_states.py +0 -0
  130. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_context_list_projects.py +0 -0
  131. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_context_list_tasks.py +0 -0
  132. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_context_resolve_binding.py +0 -0
  133. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_context_search_user.py +0 -0
  134. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_context_upsert_user_identity.py +0 -0
  135. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_db_migration.py +0 -0
  136. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_db_schema.py +0 -0
  137. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_db_session.py +0 -0
  138. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_doc_hub.py +0 -0
  139. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_doc_render_planned_versions.py +0 -0
  140. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_finalize_version_closure.py +0 -0
  141. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_loop_jobs.py +0 -0
  142. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_mind_governance.py +0 -0
  143. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_mind_proposal_extensions.py +0 -0
  144. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_mind_proposal_signoff.py +0 -0
  145. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_mind_read.py +0 -0
  146. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_monitoring_plan.py +0 -0
  147. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_monitoring_run.py +0 -0
  148. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_overview_sync_hints.py +0 -0
  149. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_pending_state_idempotency.py +0 -0
  150. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_project_group_boundary_guard.py +0 -0
  151. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_project_group_routing_contracts.py +0 -0
  152. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_release_versioning.py +0 -0
  153. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_report_build.py +0 -0
  154. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_report_persistence.py +0 -0
  155. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_runtime_bundles.py +0 -0
  156. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_runtime_output_hygiene_contracts.py +0 -0
  157. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_scheduled_run.py +0 -0
  158. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_version_closure_trigger.py +0 -0
  159. {project_agent-0.2.1 → project_agent-0.2.2}/tests/test_version_status_auto_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: project-agent
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Project Agent Python CLI core
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: typer<1,>=0.12
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "project-agent"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Project Agent Python CLI core"
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, date, datetime
4
+ from typing import Any, Protocol
5
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
6
+
7
+ from project_agent.domain.project_context.models import (
8
+ PersonalTaskSummary,
9
+ ProjectSummaryResult,
10
+ TeamTaskSummary,
11
+ )
12
+ from project_agent.domain.shared.statuses import (
13
+ is_done_status,
14
+ is_terminal_task_status,
15
+ normalize_task_status,
16
+ )
17
+
18
+
19
+ class ProjectStore(Protocol):
20
+ def find_by_id(self, project_id: str) -> Any: ...
21
+
22
+
23
+ class TaskStore(Protocol):
24
+ def list_by_project_id(self, project_id: str, task_id: str | None = None) -> list[Any]: ...
25
+
26
+
27
+ def build_project_summary(
28
+ *,
29
+ project_id: str,
30
+ user_id: str,
31
+ timezone: str | None,
32
+ project_store: ProjectStore,
33
+ task_store: TaskStore,
34
+ ) -> ProjectSummaryResult | None:
35
+ project = project_store.find_by_id(project_id)
36
+ if not project:
37
+ return None
38
+
39
+ tz = resolve_summary_timezone(timezone or project.get("timezone") or "UTC")
40
+ today = datetime.now(tz).date()
41
+ tasks = task_store.list_by_project_id(project_id)
42
+
43
+ return ProjectSummaryResult(
44
+ project_id=project["project_id"],
45
+ project_name=project["project_name"],
46
+ team_task=_build_team_task_summary(tasks, today=today, tz=tz),
47
+ personal=_build_personal_task_summary(tasks, user_id=user_id, today=today, tz=tz),
48
+ updated_at=project.get("last_updated_at"),
49
+ )
50
+
51
+
52
+ def resolve_summary_timezone(value: str) -> ZoneInfo:
53
+ try:
54
+ return ZoneInfo(value)
55
+ except ZoneInfoNotFoundError as exc:
56
+ raise ValueError(f"unknown timezone: {value}") from exc
57
+
58
+
59
+ def _build_team_task_summary(
60
+ tasks: list[Any],
61
+ *,
62
+ today: date,
63
+ tz: ZoneInfo,
64
+ ) -> TeamTaskSummary:
65
+ return TeamTaskSummary(
66
+ status="HAS_TASK" if tasks else "NO_TASK",
67
+ new_assigned_today=sum(1 for task in tasks if _is_today(_task_value(task, "created_at"), today, tz)),
68
+ completed_today=sum(
69
+ 1
70
+ for task in tasks
71
+ if is_done_status(_task_value(task, "task_status"))
72
+ and _is_today(_task_value(task, "updated_at") or _task_value(task, "last_updated_at"), today, tz)
73
+ ),
74
+ in_progress=sum(
75
+ 1
76
+ for task in tasks
77
+ if normalize_task_status(_task_value(task, "task_status")) == "in_progress"
78
+ ),
79
+ )
80
+
81
+
82
+ def _build_personal_task_summary(
83
+ tasks: list[Any],
84
+ *,
85
+ user_id: str,
86
+ today: date,
87
+ tz: ZoneInfo,
88
+ ) -> PersonalTaskSummary:
89
+ personal_tasks = [task for task in tasks if _task_value(task, "owner_user_id") == user_id]
90
+ return PersonalTaskSummary(
91
+ new_tasks_today=sum(
92
+ 1 for task in personal_tasks if _is_today(_task_value(task, "created_at"), today, tz)
93
+ ),
94
+ due_tasks_today=sum(
95
+ 1
96
+ for task in personal_tasks
97
+ if _task_due_date(_task_value(task, "end_date"), tz=tz) == today
98
+ and not is_terminal_task_status(_task_value(task, "task_status"))
99
+ ),
100
+ )
101
+
102
+
103
+ def _task_value(task: Any, key: str) -> Any:
104
+ if isinstance(task, dict):
105
+ return task.get(key)
106
+ return getattr(task, key, None)
107
+
108
+
109
+ def _is_today(value: Any, today: date, tz: ZoneInfo) -> bool:
110
+ parsed = _parse_datetime(value, tz=tz)
111
+ return parsed is not None and parsed.date() == today
112
+
113
+
114
+ def _task_due_date(value: Any, *, tz: ZoneInfo) -> date | None:
115
+ if value is None:
116
+ return None
117
+ if isinstance(value, date) and not isinstance(value, datetime):
118
+ return value
119
+ token = str(value).strip()
120
+ if not token:
121
+ return None
122
+ try:
123
+ return date.fromisoformat(token[:10])
124
+ except ValueError:
125
+ parsed = _parse_datetime(token, tz=tz)
126
+ return parsed.date() if parsed is not None else None
127
+
128
+
129
+ def _parse_datetime(value: Any, *, tz: ZoneInfo) -> datetime | None:
130
+ if value is None:
131
+ return None
132
+ if isinstance(value, datetime):
133
+ parsed = value
134
+ else:
135
+ token = str(value).strip()
136
+ if not token:
137
+ return None
138
+ try:
139
+ parsed = datetime.fromisoformat(token.replace("Z", "+00:00"))
140
+ except ValueError:
141
+ return None
142
+ if parsed.tzinfo is None:
143
+ parsed = parsed.replace(tzinfo=UTC)
144
+ return parsed.astimezone(tz)
@@ -137,6 +137,32 @@ class GetTaskResult(ProjectAgentModel):
137
137
  task: TaskSummary = Field(description="命中的单条 Task 详情。")
138
138
 
139
139
 
140
+ class TeamTaskSummary(ProjectAgentModel):
141
+ """项目团队任务汇总。"""
142
+
143
+ status: str = Field(description="团队任务状态,例如 HAS_TASK 或 NO_TASK。")
144
+ new_assigned_today: int = Field(description="今天新分配到项目的任务数量。")
145
+ completed_today: int = Field(description="今天完成的项目任务数量。")
146
+ in_progress: int = Field(description="进行中的项目任务数量。")
147
+
148
+
149
+ class PersonalTaskSummary(ProjectAgentModel):
150
+ """当前用户在项目内的任务汇总。"""
151
+
152
+ new_tasks_today: int = Field(description="今天分配给当前用户的新任务数量。")
153
+ due_tasks_today: int = Field(description="今天到期且未终态的当前用户任务数量。")
154
+
155
+
156
+ class ProjectSummaryResult(ProjectAgentModel):
157
+ """`context project-summary` 的结构化输出。"""
158
+
159
+ project_id: str = Field(description="项目内部唯一标识。")
160
+ project_name: str = Field(description="项目名称。")
161
+ team_task: TeamTaskSummary = Field(description="团队任务汇总。")
162
+ personal: PersonalTaskSummary = Field(description="当前用户任务汇总。")
163
+ updated_at: str | None = Field(default=None, description="项目 UTC 更新时间。")
164
+
165
+
140
166
  class PendingStateSummary(ProjectAgentModel):
141
167
  """项目上下文里暴露给 router 或 gate 的 pending state 摘要。"""
142
168
 
@@ -1914,6 +1914,8 @@ def _to_participant_row(record: ProjectParticipantRecord) -> dict:
1914
1914
  "user_id": record.user_id,
1915
1915
  "role": record.role,
1916
1916
  "status": record.status,
1917
+ "created_at": _dump_datetime(record.created_at),
1918
+ "updated_at": _dump_datetime(record.updated_at),
1917
1919
  }
1918
1920
 
1919
1921
 
@@ -1978,6 +1980,8 @@ def _to_task_row(record: ProjectTaskRecord) -> dict:
1978
1980
  "start_date": record.start_date,
1979
1981
  "end_date": record.end_date,
1980
1982
  "last_updated_at": _dump_datetime(record.last_updated_at),
1983
+ "created_at": _dump_datetime(record.created_at),
1984
+ "updated_at": _dump_datetime(record.updated_at),
1981
1985
  "latest_update": payload.get("latest_update"),
1982
1986
  "has_effective_update": payload.get("has_effective_update"),
1983
1987
  "effective_update_fields": payload.get("effective_update_fields", []),
@@ -10,6 +10,7 @@ from project_agent.application.context.get_task import get_task as get_task_serv
10
10
  from project_agent.application.context.get_user_context import get_user_context as get_user_context_service
11
11
  from project_agent.application.context.list_tasks import list_tasks as list_tasks_service
12
12
  from project_agent.application.context.list_projects import list_projects as list_projects_service
13
+ from project_agent.application.context.project_summary import build_project_summary as build_project_summary_service
13
14
  from project_agent.application.context.resolve_group import resolve_group as resolve_group_service
14
15
  from project_agent.application.context.resolve_user import resolve_user as resolve_user_service
15
16
  from project_agent.application.context.search_user import search_user as search_user_service
@@ -101,6 +102,14 @@ class ListTasksCommandInput(BaseModel):
101
102
  external_subject_id: str | None = None
102
103
 
103
104
 
105
+ class ProjectSummaryCommandInput(BaseModel):
106
+ project_id: str
107
+ user_id: str | None = None
108
+ channel: str | None = None
109
+ external_subject_id: str | None = None
110
+ timezone: str | None = None
111
+
112
+
104
113
  class ResolveUserCommandInput(BaseModel):
105
114
  channel: str
106
115
  external_subject_id: str
@@ -378,6 +387,68 @@ def list_projects(
378
387
  typer.echo(json.dumps(result, indent=2, ensure_ascii=True))
379
388
 
380
389
 
390
+ @app.command("project-summary", help="读取项目团队与当前用户的任务汇总。")
391
+ def project_summary(
392
+ project_id: str = project_id_option(),
393
+ user_id: str | None = typer.Option(None, "--user-id", help="内部 canonical userId。"),
394
+ channel: str | None = channel_option(),
395
+ external_subject_id: str | None = typer.Option(None, "--external-subject-id", help="渠道侧主体 ID。与 --channel 配合后可直接解析内部 userId。"),
396
+ timezone: str | None = typer.Option(None, "--timezone", help="用于计算今日统计的 IANA 时区,例如 Asia/Shanghai。"),
397
+ db_path: str | None = db_path_option(),
398
+ ) -> None:
399
+ payload = ProjectSummaryCommandInput(
400
+ project_id=project_id,
401
+ user_id=user_id,
402
+ channel=channel,
403
+ external_subject_id=external_subject_id,
404
+ timezone=timezone,
405
+ )
406
+ _validate_external_identity_args(
407
+ channel=payload.channel,
408
+ external_subject_id=payload.external_subject_id,
409
+ )
410
+ if payload.user_id is not None and payload.external_subject_id is not None:
411
+ _echo_invalid_input("user_id cannot be used together with channel/external_subject_id")
412
+ raise typer.Exit(code=1)
413
+ if payload.user_id is None and payload.external_subject_id is None:
414
+ _echo_invalid_input("project-summary requires --user-id or --channel with --external-subject-id")
415
+ raise typer.Exit(code=1)
416
+
417
+ with open_session(db_path) as session:
418
+ resolved_user_id = payload.user_id
419
+ if payload.external_subject_id is not None:
420
+ resolved_user_id = _resolve_user_id_or_exit(
421
+ session=session,
422
+ channel=payload.channel,
423
+ external_subject_id=payload.external_subject_id,
424
+ )
425
+ if resolved_user_id is None:
426
+ _echo_invalid_input("project-summary requires a resolved user_id")
427
+ raise typer.Exit(code=1)
428
+ try:
429
+ result = build_project_summary_service(
430
+ project_id=payload.project_id,
431
+ user_id=resolved_user_id,
432
+ timezone=payload.timezone,
433
+ project_store=SqlAlchemyProjectStore(session),
434
+ task_store=SqlAlchemyTaskStore(session),
435
+ )
436
+ except ValueError as exc:
437
+ _echo_invalid_input(str(exc))
438
+ raise typer.Exit(code=1) from exc
439
+
440
+ if result is None:
441
+ typer.echo(
442
+ '{\n'
443
+ ' "status": "not_found",\n'
444
+ f' "projectId": "{payload.project_id}"\n'
445
+ '}'
446
+ )
447
+ raise typer.Exit(code=1)
448
+
449
+ typer.echo(result.model_dump_json(indent=2, by_alias=True))
450
+
451
+
381
452
  @app.command("get-project-owner", help="按项目 ID 查询项目 Owner 用户详情及渠道身份映射。")
382
453
  def get_project_owner(
383
454
  project_id: str = project_id_option(),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: project-agent
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Project Agent Python CLI core
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: typer<1,>=0.12
@@ -20,6 +20,7 @@ src/project_agent/application/context/get_task.py
20
20
  src/project_agent/application/context/get_user_context.py
21
21
  src/project_agent/application/context/list_projects.py
22
22
  src/project_agent/application/context/list_tasks.py
23
+ src/project_agent/application/context/project_summary.py
23
24
  src/project_agent/application/context/resolve_binding.py
24
25
  src/project_agent/application/context/resolve_group.py
25
26
  src/project_agent/application/context/resolve_user.py
@@ -125,6 +126,7 @@ tests/test_context_get_project_owner.py
125
126
  tests/test_context_get_project_pending_states.py
126
127
  tests/test_context_list_projects.py
127
128
  tests/test_context_list_tasks.py
129
+ tests/test_context_project_summary.py
128
130
  tests/test_context_resolve_binding.py
129
131
  tests/test_context_search_user.py
130
132
  tests/test_context_upsert_user_identity.py
@@ -0,0 +1,322 @@
1
+ import json
2
+ import sqlite3
3
+ import sys
4
+ from datetime import UTC, datetime, timedelta
5
+ from pathlib import Path
6
+
7
+ sys.path.insert(0, str(Path(__file__).resolve().parents[4]))
8
+
9
+ from fastapi import FastAPI
10
+ from fastapi.testclient import TestClient
11
+ from typer.testing import CliRunner
12
+
13
+ from project_agent.infrastructure.db.schema import create_schema
14
+ from project_agent.interface.cli.app import app
15
+ from runtime.mushroomAgent.deploy.project_agent_api import create_router
16
+
17
+
18
+ runner = CliRunner()
19
+
20
+
21
+ def test_project_summary_cli_resolves_external_subject_and_returns_counts(tmp_path) -> None:
22
+ db_path = tmp_path / "project-agent.db"
23
+ create_schema(str(db_path))
24
+ now = datetime.now(UTC).replace(tzinfo=None)
25
+ _insert_user(db_path, user_id="usr_member_001", display_name="Member One")
26
+ _insert_identity(
27
+ db_path,
28
+ identity_id="uci_member_001",
29
+ user_id="usr_member_001",
30
+ channel="openim",
31
+ external_subject_id="open_user_001",
32
+ )
33
+ _insert_project(db_path, project_id="proj_summary_001", updated_at=now)
34
+ _insert_task(
35
+ db_path,
36
+ task_id="task_owned_due",
37
+ project_id="proj_summary_001",
38
+ owner_user_id="usr_member_001",
39
+ task_status="in_progress",
40
+ end_date=now.date().isoformat(),
41
+ created_at=now,
42
+ updated_at=now,
43
+ )
44
+ _insert_task(
45
+ db_path,
46
+ task_id="task_done_today",
47
+ project_id="proj_summary_001",
48
+ owner_user_id="usr_other_001",
49
+ task_status="done",
50
+ end_date=now.date().isoformat(),
51
+ created_at=now - timedelta(days=1),
52
+ updated_at=now,
53
+ )
54
+
55
+ result = runner.invoke(
56
+ app,
57
+ [
58
+ "context",
59
+ "project-summary",
60
+ "--project-id",
61
+ "proj_summary_001",
62
+ "--channel",
63
+ "openim",
64
+ "--external-subject-id",
65
+ "open_user_001",
66
+ "--timezone",
67
+ "UTC",
68
+ "--db-path",
69
+ str(db_path),
70
+ ],
71
+ )
72
+
73
+ assert result.exit_code == 0
74
+ payload = json.loads(result.stdout)
75
+ assert payload["projectId"] == "proj_summary_001"
76
+ assert payload["projectName"] == "Summary Project"
77
+ assert payload["teamTask"] == {
78
+ "status": "HAS_TASK",
79
+ "newAssignedToday": 1,
80
+ "completedToday": 1,
81
+ "inProgress": 1,
82
+ }
83
+ assert payload["personal"] == {
84
+ "newTasksToday": 1,
85
+ "dueTasksToday": 1,
86
+ }
87
+ assert payload["updatedAt"].endswith("Z")
88
+
89
+
90
+ def test_project_summary_api_uses_backend_summary_service(tmp_path) -> None:
91
+ db_path = tmp_path / "project-agent.db"
92
+ create_schema(str(db_path))
93
+ now = datetime.now(UTC).replace(tzinfo=None)
94
+ _insert_user(db_path, user_id="usr_member_001", display_name="Member One")
95
+ _insert_identity(
96
+ db_path,
97
+ identity_id="uci_member_001",
98
+ user_id="usr_member_001",
99
+ channel="openim",
100
+ external_subject_id="open_user_001",
101
+ )
102
+ _insert_project(db_path, project_id="proj_summary_001", updated_at=now)
103
+ _insert_binding(
104
+ db_path,
105
+ binding_id="bind_summary_001",
106
+ project_id="proj_summary_001",
107
+ channel="openim",
108
+ external_channel_id="group_summary_001",
109
+ )
110
+ _insert_task(
111
+ db_path,
112
+ task_id="task_owned_due",
113
+ project_id="proj_summary_001",
114
+ owner_user_id="usr_member_001",
115
+ task_status="in_progress",
116
+ end_date=now.date().isoformat(),
117
+ created_at=now,
118
+ updated_at=now,
119
+ )
120
+
121
+ fastapi_app = FastAPI()
122
+ fastapi_app.include_router(create_router(None, {"db_path": str(db_path)}))
123
+ response = TestClient(fastapi_app).get(
124
+ "/api/projects/summary",
125
+ params={
126
+ "groupId": "group_summary_001",
127
+ "userId": "open_user_001",
128
+ "channel": "openim",
129
+ "timezone": "UTC",
130
+ },
131
+ )
132
+
133
+ assert response.status_code == 200
134
+ assert response.json()["personal"] == {
135
+ "newTasksToday": 1,
136
+ "dueTasksToday": 1,
137
+ }
138
+
139
+
140
+ def test_project_summary_cli_rejects_missing_user_identity(tmp_path) -> None:
141
+ db_path = tmp_path / "project-agent.db"
142
+ create_schema(str(db_path))
143
+ _insert_project(db_path, project_id="proj_summary_001", updated_at=datetime.now(UTC).replace(tzinfo=None))
144
+
145
+ result = runner.invoke(
146
+ app,
147
+ [
148
+ "context",
149
+ "project-summary",
150
+ "--project-id",
151
+ "proj_summary_001",
152
+ "--timezone",
153
+ "UTC",
154
+ "--db-path",
155
+ str(db_path),
156
+ ],
157
+ )
158
+
159
+ assert result.exit_code == 1
160
+ assert json.loads(result.stdout)["message"] == (
161
+ "project-summary requires --user-id or --channel with --external-subject-id"
162
+ )
163
+
164
+
165
+ def _insert_project(db_path, *, project_id: str, updated_at: datetime) -> None:
166
+ with sqlite3.connect(db_path) as connection:
167
+ connection.execute(
168
+ """
169
+ INSERT INTO projects (
170
+ project_id,
171
+ project_name,
172
+ owner_user_id,
173
+ primary_goal,
174
+ mind_stage,
175
+ lifecycle_status,
176
+ current_revision_id,
177
+ current_version_id,
178
+ timezone,
179
+ created_at,
180
+ updated_at
181
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
182
+ """,
183
+ (
184
+ project_id,
185
+ "Summary Project",
186
+ "usr_owner",
187
+ "Summarize project tasks",
188
+ "v1",
189
+ "active",
190
+ "rev_summary_001",
191
+ "ver_summary_001",
192
+ "UTC",
193
+ _db_datetime(updated_at),
194
+ _db_datetime(updated_at),
195
+ ),
196
+ )
197
+ connection.commit()
198
+
199
+
200
+ def _insert_user(db_path, *, user_id: str, display_name: str) -> None:
201
+ with sqlite3.connect(db_path) as connection:
202
+ connection.execute(
203
+ """
204
+ INSERT INTO users (
205
+ user_id,
206
+ display_name,
207
+ status,
208
+ created_at,
209
+ updated_at
210
+ ) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
211
+ """,
212
+ (user_id, display_name, "active"),
213
+ )
214
+ connection.commit()
215
+
216
+
217
+ def _insert_identity(
218
+ db_path,
219
+ *,
220
+ identity_id: str,
221
+ user_id: str,
222
+ channel: str,
223
+ external_subject_id: str,
224
+ ) -> None:
225
+ with sqlite3.connect(db_path) as connection:
226
+ connection.execute(
227
+ """
228
+ INSERT INTO user_channel_identities (
229
+ identity_id,
230
+ user_id,
231
+ channel,
232
+ external_subject_id,
233
+ status,
234
+ profile_json,
235
+ created_at,
236
+ updated_at
237
+ ) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
238
+ """,
239
+ (identity_id, user_id, channel, external_subject_id, "active", "{}"),
240
+ )
241
+ connection.commit()
242
+
243
+
244
+ def _insert_binding(
245
+ db_path,
246
+ *,
247
+ binding_id: str,
248
+ project_id: str,
249
+ channel: str,
250
+ external_channel_id: str,
251
+ ) -> None:
252
+ with sqlite3.connect(db_path) as connection:
253
+ connection.execute(
254
+ """
255
+ INSERT INTO project_bindings (
256
+ binding_id,
257
+ project_id,
258
+ channel,
259
+ external_channel_id,
260
+ binding_kind,
261
+ status,
262
+ payload_json,
263
+ created_at,
264
+ updated_at
265
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
266
+ """,
267
+ (binding_id, project_id, channel, external_channel_id, "project_group", "active", "{}"),
268
+ )
269
+ connection.commit()
270
+
271
+
272
+ def _insert_task(
273
+ db_path,
274
+ *,
275
+ task_id: str,
276
+ project_id: str,
277
+ owner_user_id: str,
278
+ task_status: str,
279
+ end_date: str,
280
+ created_at: datetime,
281
+ updated_at: datetime,
282
+ ) -> None:
283
+ with sqlite3.connect(db_path) as connection:
284
+ connection.execute(
285
+ """
286
+ INSERT INTO project_tasks (
287
+ task_id,
288
+ project_id,
289
+ version_id,
290
+ task_name,
291
+ task_description,
292
+ owner_user_id,
293
+ task_status,
294
+ start_date,
295
+ end_date,
296
+ last_updated_at,
297
+ payload_json,
298
+ created_at,
299
+ updated_at
300
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
301
+ """,
302
+ (
303
+ task_id,
304
+ project_id,
305
+ "ver_summary_001",
306
+ task_id,
307
+ f"{task_id} description",
308
+ owner_user_id,
309
+ task_status,
310
+ created_at.date().isoformat(),
311
+ end_date,
312
+ _db_datetime(updated_at),
313
+ "{}",
314
+ _db_datetime(created_at),
315
+ _db_datetime(updated_at),
316
+ ),
317
+ )
318
+ connection.commit()
319
+
320
+
321
+ def _db_datetime(value: datetime) -> str:
322
+ return value.isoformat(sep=" ")
File without changes
File without changes