project-agent 0.1.22__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.
- project_agent-0.1.22/PKG-INFO +11 -0
- project_agent-0.1.22/pyproject.toml +31 -0
- project_agent-0.1.22/setup.cfg +4 -0
- project_agent-0.1.22/src/application/__init__.py +1 -0
- project_agent-0.1.22/src/application/bootstrap/__init__.py +1 -0
- project_agent-0.1.22/src/application/bootstrap/checkpoint.py +376 -0
- project_agent-0.1.22/src/application/bootstrap/init.py +201 -0
- project_agent-0.1.22/src/application/bootstrap/loop_state.py +278 -0
- project_agent-0.1.22/src/application/context/__init__.py +1 -0
- project_agent-0.1.22/src/application/context/binding_normalization.py +70 -0
- project_agent-0.1.22/src/application/context/get_project_context.py +59 -0
- project_agent-0.1.22/src/application/context/get_project_owner.py +53 -0
- project_agent-0.1.22/src/application/context/get_task.py +15 -0
- project_agent-0.1.22/src/application/context/get_user_context.py +33 -0
- project_agent-0.1.22/src/application/context/list_projects.py +222 -0
- project_agent-0.1.22/src/application/context/list_tasks.py +35 -0
- project_agent-0.1.22/src/application/context/resolve_binding.py +266 -0
- project_agent-0.1.22/src/application/context/resolve_group.py +29 -0
- project_agent-0.1.22/src/application/context/resolve_user.py +54 -0
- project_agent-0.1.22/src/application/context/search_user.py +161 -0
- project_agent-0.1.22/src/application/context/upsert_user_identity.py +48 -0
- project_agent-0.1.22/src/application/doc/__init__.py +1 -0
- project_agent-0.1.22/src/application/doc/render.py +1004 -0
- project_agent-0.1.22/src/application/doc/sync.py +137 -0
- project_agent-0.1.22/src/application/doc/writeback.py +73 -0
- project_agent-0.1.22/src/application/doc_hub/__init__.py +1 -0
- project_agent-0.1.22/src/application/doc_hub/manage.py +489 -0
- project_agent-0.1.22/src/application/doc_hub/read.py +347 -0
- project_agent-0.1.22/src/application/loop/__init__.py +1 -0
- project_agent-0.1.22/src/application/loop/enqueue.py +141 -0
- project_agent-0.1.22/src/application/loop/run.py +303 -0
- project_agent-0.1.22/src/application/mind/__init__.py +1 -0
- project_agent-0.1.22/src/application/mind/create_version.py +226 -0
- project_agent-0.1.22/src/application/mind/delete_task.py +146 -0
- project_agent-0.1.22/src/application/mind/finalize_version_closure.py +443 -0
- project_agent-0.1.22/src/application/mind/milestones.py +134 -0
- project_agent-0.1.22/src/application/mind/planned_versions.py +32 -0
- project_agent-0.1.22/src/application/mind/propose_change.py +449 -0
- project_agent-0.1.22/src/application/mind/read.py +405 -0
- project_agent-0.1.22/src/application/mind/set_tracking.py +65 -0
- project_agent-0.1.22/src/application/mind/signoff_proposal.py +303 -0
- project_agent-0.1.22/src/application/mind/snooze_task_reminder.py +168 -0
- project_agent-0.1.22/src/application/mind/switch_version.py +131 -0
- project_agent-0.1.22/src/application/mind/update_version.py +257 -0
- project_agent-0.1.22/src/application/mind/write_task_update.py +314 -0
- project_agent-0.1.22/src/application/monitoring/__init__.py +1 -0
- project_agent-0.1.22/src/application/monitoring/evaluate_followups.py +104 -0
- project_agent-0.1.22/src/application/monitoring/plan.py +40 -0
- project_agent-0.1.22/src/application/monitoring/run.py +153 -0
- project_agent-0.1.22/src/application/report/__init__.py +1 -0
- project_agent-0.1.22/src/application/report/build.py +924 -0
- project_agent-0.1.22/src/application/scheduled/__init__.py +3 -0
- project_agent-0.1.22/src/application/scheduled/run.py +667 -0
- project_agent-0.1.22/src/application/version_closure/__init__.py +3 -0
- project_agent-0.1.22/src/application/version_closure/evaluate.py +86 -0
- project_agent-0.1.22/src/domain/__init__.py +1 -0
- project_agent-0.1.22/src/domain/bootstrap/__init__.py +1 -0
- project_agent-0.1.22/src/domain/bootstrap/models.py +141 -0
- project_agent-0.1.22/src/domain/doc/__init__.py +1 -0
- project_agent-0.1.22/src/domain/doc/models.py +73 -0
- project_agent-0.1.22/src/domain/doc_hub/__init__.py +1 -0
- project_agent-0.1.22/src/domain/doc_hub/models.py +159 -0
- project_agent-0.1.22/src/domain/loop/__init__.py +1 -0
- project_agent-0.1.22/src/domain/loop/models.py +50 -0
- project_agent-0.1.22/src/domain/mind/__init__.py +1 -0
- project_agent-0.1.22/src/domain/mind/models.py +255 -0
- project_agent-0.1.22/src/domain/monitoring/__init__.py +1 -0
- project_agent-0.1.22/src/domain/monitoring/evaluator.py +846 -0
- project_agent-0.1.22/src/domain/monitoring/models.py +270 -0
- project_agent-0.1.22/src/domain/project_context/__init__.py +1 -0
- project_agent-0.1.22/src/domain/project_context/models.py +178 -0
- project_agent-0.1.22/src/domain/report/__init__.py +1 -0
- project_agent-0.1.22/src/domain/report/models.py +210 -0
- project_agent-0.1.22/src/domain/scheduled/__init__.py +3 -0
- project_agent-0.1.22/src/domain/scheduled/models.py +70 -0
- project_agent-0.1.22/src/domain/shared/__init__.py +1 -0
- project_agent-0.1.22/src/domain/shared/documents.py +21 -0
- project_agent-0.1.22/src/domain/shared/models.py +14 -0
- project_agent-0.1.22/src/domain/shared/project_mind_derived.py +143 -0
- project_agent-0.1.22/src/domain/shared/project_mind_readiness.py +67 -0
- project_agent-0.1.22/src/domain/shared/statuses.py +50 -0
- project_agent-0.1.22/src/domain/user/__init__.py +1 -0
- project_agent-0.1.22/src/domain/user/models.py +130 -0
- project_agent-0.1.22/src/domain/version_closure/__init__.py +13 -0
- project_agent-0.1.22/src/domain/version_closure/evaluator.py +284 -0
- project_agent-0.1.22/src/domain/version_closure/models.py +104 -0
- project_agent-0.1.22/src/infrastructure/__init__.py +1 -0
- project_agent-0.1.22/src/infrastructure/clock/__init__.py +1 -0
- project_agent-0.1.22/src/infrastructure/clock/workday_clock.py +30 -0
- project_agent-0.1.22/src/infrastructure/db/__init__.py +1 -0
- project_agent-0.1.22/src/infrastructure/db/migration.py +154 -0
- project_agent-0.1.22/src/infrastructure/db/repositories.py +2152 -0
- project_agent-0.1.22/src/infrastructure/db/schema.py +609 -0
- project_agent-0.1.22/src/infrastructure/db/session.py +80 -0
- project_agent-0.1.22/src/infrastructure/db/tables.py +529 -0
- project_agent-0.1.22/src/interface/__init__.py +1 -0
- project_agent-0.1.22/src/interface/cli/__init__.py +1 -0
- project_agent-0.1.22/src/interface/cli/app.py +84 -0
- project_agent-0.1.22/src/interface/cli/commands/__init__.py +1 -0
- project_agent-0.1.22/src/interface/cli/commands/bootstrap.py +296 -0
- project_agent-0.1.22/src/interface/cli/commands/context.py +603 -0
- project_agent-0.1.22/src/interface/cli/commands/db.py +28 -0
- project_agent-0.1.22/src/interface/cli/commands/doc.py +283 -0
- project_agent-0.1.22/src/interface/cli/commands/doc_hub.py +575 -0
- project_agent-0.1.22/src/interface/cli/commands/loop.py +123 -0
- project_agent-0.1.22/src/interface/cli/commands/mind.py +1250 -0
- project_agent-0.1.22/src/interface/cli/commands/monitoring.py +165 -0
- project_agent-0.1.22/src/interface/cli/commands/project_boundary.py +134 -0
- project_agent-0.1.22/src/interface/cli/commands/report.py +99 -0
- project_agent-0.1.22/src/interface/cli/commands/scheduled.py +81 -0
- project_agent-0.1.22/src/interface/cli/commands/task_event_notify.py +221 -0
- project_agent-0.1.22/src/interface/cli/commands/upgrade.py +183 -0
- project_agent-0.1.22/src/interface/cli/commands/version_closure.py +86 -0
- project_agent-0.1.22/src/interface/cli/helptext.py +112 -0
- project_agent-0.1.22/src/project_agent.egg-info/PKG-INFO +11 -0
- project_agent-0.1.22/src/project_agent.egg-info/SOURCES.txt +153 -0
- project_agent-0.1.22/src/project_agent.egg-info/dependency_links.txt +1 -0
- project_agent-0.1.22/src/project_agent.egg-info/entry_points.txt +2 -0
- project_agent-0.1.22/src/project_agent.egg-info/requires.txt +6 -0
- project_agent-0.1.22/src/project_agent.egg-info/top_level.txt +4 -0
- project_agent-0.1.22/tests/test_bootstrap_checkpoint.py +830 -0
- project_agent-0.1.22/tests/test_cli_help.py +194 -0
- project_agent-0.1.22/tests/test_cli_version.py +47 -0
- project_agent-0.1.22/tests/test_context_get_project_owner.py +203 -0
- project_agent-0.1.22/tests/test_context_get_project_pending_states.py +179 -0
- project_agent-0.1.22/tests/test_context_list_projects.py +571 -0
- project_agent-0.1.22/tests/test_context_list_tasks.py +475 -0
- project_agent-0.1.22/tests/test_context_resolve_binding.py +485 -0
- project_agent-0.1.22/tests/test_context_search_user.py +188 -0
- project_agent-0.1.22/tests/test_context_upsert_user_identity.py +174 -0
- project_agent-0.1.22/tests/test_db_migration.py +242 -0
- project_agent-0.1.22/tests/test_db_schema.py +374 -0
- project_agent-0.1.22/tests/test_db_session.py +66 -0
- project_agent-0.1.22/tests/test_doc_hub.py +1604 -0
- project_agent-0.1.22/tests/test_doc_render_planned_versions.py +498 -0
- project_agent-0.1.22/tests/test_finalize_version_closure.py +771 -0
- project_agent-0.1.22/tests/test_loop_jobs.py +169 -0
- project_agent-0.1.22/tests/test_mind_governance.py +1112 -0
- project_agent-0.1.22/tests/test_mind_proposal_extensions.py +763 -0
- project_agent-0.1.22/tests/test_mind_proposal_signoff.py +452 -0
- project_agent-0.1.22/tests/test_mind_read.py +1404 -0
- project_agent-0.1.22/tests/test_monitoring_plan.py +149 -0
- project_agent-0.1.22/tests/test_monitoring_run.py +789 -0
- project_agent-0.1.22/tests/test_overview_sync_hints.py +356 -0
- project_agent-0.1.22/tests/test_pending_state_idempotency.py +125 -0
- project_agent-0.1.22/tests/test_project_group_boundary_guard.py +836 -0
- project_agent-0.1.22/tests/test_project_group_routing_contracts.py +150 -0
- project_agent-0.1.22/tests/test_release_versioning.py +333 -0
- project_agent-0.1.22/tests/test_report_build.py +1310 -0
- project_agent-0.1.22/tests/test_report_persistence.py +653 -0
- project_agent-0.1.22/tests/test_runtime_bundles.py +468 -0
- project_agent-0.1.22/tests/test_runtime_output_hygiene_contracts.py +77 -0
- project_agent-0.1.22/tests/test_scheduled_run.py +2627 -0
- project_agent-0.1.22/tests/test_version_closure_trigger.py +777 -0
- project_agent-0.1.22/tests/test_version_status_auto_sync.py +424 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: project-agent
|
|
3
|
+
Version: 0.1.22
|
|
4
|
+
Summary: Project Agent Python CLI core
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: typer<1,>=0.12
|
|
7
|
+
Requires-Dist: pydantic<3,>=2.8
|
|
8
|
+
Requires-Dist: sqlalchemy<3,>=2.0
|
|
9
|
+
Requires-Dist: pymysql<2,>=1.1
|
|
10
|
+
Requires-Dist: pendulum<4,>=3
|
|
11
|
+
Requires-Dist: chinesecalendar<2,>=1.10
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "project-agent"
|
|
7
|
+
version = "0.1.22"
|
|
8
|
+
description = "Project Agent Python CLI core"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"typer>=0.12,<1",
|
|
12
|
+
"pydantic>=2.8,<3",
|
|
13
|
+
"sqlalchemy>=2.0,<3",
|
|
14
|
+
"pymysql>=1.1,<2",
|
|
15
|
+
"pendulum>=3,<4",
|
|
16
|
+
"chinesecalendar>=1.10,<2",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=8.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
project-agent = "interface.cli.app:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools]
|
|
28
|
+
package-dir = {"" = "src"}
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Application layer for Project Agent."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Bootstrap application services."""
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
from typing import Any, Protocol
|
|
2
|
+
|
|
3
|
+
import pendulum
|
|
4
|
+
|
|
5
|
+
from domain.bootstrap.models import (
|
|
6
|
+
BootstrapCheckpointResult,
|
|
7
|
+
BootstrapMarkResult,
|
|
8
|
+
BootstrapMilestones,
|
|
9
|
+
)
|
|
10
|
+
from domain.shared.project_mind_derived import (
|
|
11
|
+
describe_missing_task_fields,
|
|
12
|
+
describe_missing_version_fields,
|
|
13
|
+
resolve_current_version,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
MILESTONE_ALIGNMENT_INTRO = "alignment_intro_sent"
|
|
17
|
+
MILESTONE_EXECUTION_CONFIRMATION = "execution_confirmation_sent"
|
|
18
|
+
MILESTONE_KICKOFF = "kickoff_sent"
|
|
19
|
+
|
|
20
|
+
REQUIRED_BOOTSTRAP_DOC_KINDS = ("project_card", "docs_index", "project_overview")
|
|
21
|
+
|
|
22
|
+
_MILESTONE_STATE_KINDS = {
|
|
23
|
+
MILESTONE_ALIGNMENT_INTRO: "bootstrap_alignment_intro",
|
|
24
|
+
MILESTONE_EXECUTION_CONFIRMATION: "bootstrap_execution_confirmation",
|
|
25
|
+
MILESTONE_KICKOFF: "bootstrap_kickoff",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BootstrapReadStore(Protocol):
|
|
30
|
+
def find_project_by_id(self, project_id: str) -> Any: ...
|
|
31
|
+
|
|
32
|
+
def list_bindings_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
33
|
+
|
|
34
|
+
def list_versions_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
35
|
+
|
|
36
|
+
def list_tasks_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
37
|
+
|
|
38
|
+
def list_documents_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PendingStateStore(Protocol):
|
|
42
|
+
def upsert_open_state(self, payload: dict) -> Any: ...
|
|
43
|
+
|
|
44
|
+
def resolve_open_state(self, *, project_id: str, state_key: str) -> Any: ...
|
|
45
|
+
|
|
46
|
+
def find_latest_by_state_key(self, *, project_id: str, state_key: str) -> Any: ...
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def alignment_intro_state_key(project_id: str) -> str:
|
|
50
|
+
return f"project:{project_id}:bootstrap:alignment_intro"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def execution_confirmation_state_key(project_id: str) -> str:
|
|
54
|
+
return f"project:{project_id}:bootstrap:execution_confirmation"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def kickoff_state_key(project_id: str) -> str:
|
|
58
|
+
return f"project:{project_id}:bootstrap:kickoff"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_STATE_KEY_BUILDERS = {
|
|
62
|
+
MILESTONE_ALIGNMENT_INTRO: alignment_intro_state_key,
|
|
63
|
+
MILESTONE_EXECUTION_CONFIRMATION: execution_confirmation_state_key,
|
|
64
|
+
MILESTONE_KICKOFF: kickoff_state_key,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_alignment_intro_pending(
|
|
69
|
+
*,
|
|
70
|
+
project_id: str,
|
|
71
|
+
pending_state_store: PendingStateStore,
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""群绑定落地时登记"欠一条对齐引导";已有任何落账记录(含已发)时不重复登记。"""
|
|
74
|
+
state_key = alignment_intro_state_key(project_id)
|
|
75
|
+
existing = pending_state_store.find_latest_by_state_key(
|
|
76
|
+
project_id=project_id, state_key=state_key
|
|
77
|
+
)
|
|
78
|
+
if existing is not None:
|
|
79
|
+
return False
|
|
80
|
+
pending_state_store.upsert_open_state(
|
|
81
|
+
{
|
|
82
|
+
"project_id": project_id,
|
|
83
|
+
"scope_kind": "project",
|
|
84
|
+
"scope_ref": project_id,
|
|
85
|
+
"state_kind": _MILESTONE_STATE_KINDS[MILESTONE_ALIGNMENT_INTRO],
|
|
86
|
+
"state_key": state_key,
|
|
87
|
+
"payload_json": {
|
|
88
|
+
"milestone": MILESTONE_ALIGNMENT_INTRO,
|
|
89
|
+
"openedAt": _now_iso(),
|
|
90
|
+
},
|
|
91
|
+
"status": "open",
|
|
92
|
+
"expires_at": None,
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def evaluate_bootstrap_checkpoint(
|
|
99
|
+
*,
|
|
100
|
+
project_id: str,
|
|
101
|
+
bootstrap_store: BootstrapReadStore,
|
|
102
|
+
pending_state_store: PendingStateStore,
|
|
103
|
+
) -> BootstrapCheckpointResult | None:
|
|
104
|
+
project = bootstrap_store.find_project_by_id(project_id)
|
|
105
|
+
if project is None:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
bindings = bootstrap_store.list_bindings_by_project_id(project_id)
|
|
109
|
+
has_group_binding = any(
|
|
110
|
+
binding.get("binding_kind") == "project_group" and binding.get("status") == "active"
|
|
111
|
+
for binding in bindings
|
|
112
|
+
)
|
|
113
|
+
documents = bootstrap_store.list_documents_by_project_id(project_id)
|
|
114
|
+
landed_doc_kinds = {document.get("doc_kind") for document in documents}
|
|
115
|
+
has_project_card_doc = "project_card" in landed_doc_kinds
|
|
116
|
+
doc_gaps = [kind for kind in REQUIRED_BOOTSTRAP_DOC_KINDS if kind not in landed_doc_kinds]
|
|
117
|
+
|
|
118
|
+
versions = bootstrap_store.list_versions_by_project_id(project_id)
|
|
119
|
+
tasks = bootstrap_store.list_tasks_by_project_id(project_id)
|
|
120
|
+
alignment_gaps = _build_alignment_gaps(project=project, versions=versions, tasks=tasks)
|
|
121
|
+
alignment_ready = _is_alignment_ready(project=project, versions=versions, tasks=tasks)
|
|
122
|
+
|
|
123
|
+
mind_stage = project.get("mind_stage") or "v0"
|
|
124
|
+
kickoff_sent = _is_milestone_marked(
|
|
125
|
+
project_id=project_id,
|
|
126
|
+
state_key=kickoff_state_key(project_id),
|
|
127
|
+
pending_state_store=pending_state_store,
|
|
128
|
+
)
|
|
129
|
+
# bootstrap 是否仍在进行,以 kickoff 是否已落账为准:create-version 会在团队对齐期
|
|
130
|
+
# 把 mind_stage 提前翻成 v1,但只要 kickoff 还没发,bootstrap 就还没结束。
|
|
131
|
+
bootstrap_active = not kickoff_sent
|
|
132
|
+
|
|
133
|
+
intro_status = _reconcile_alignment_intro(
|
|
134
|
+
project_id=project_id,
|
|
135
|
+
bootstrap_active=bootstrap_active,
|
|
136
|
+
has_group_binding=has_group_binding,
|
|
137
|
+
pending_state_store=pending_state_store,
|
|
138
|
+
)
|
|
139
|
+
confirmation_status = _reconcile_execution_confirmation(
|
|
140
|
+
project_id=project_id,
|
|
141
|
+
bootstrap_active=bootstrap_active,
|
|
142
|
+
intro_status=intro_status,
|
|
143
|
+
alignment_ready=alignment_ready,
|
|
144
|
+
pending_state_store=pending_state_store,
|
|
145
|
+
)
|
|
146
|
+
kickoff_status = _reconcile_kickoff(
|
|
147
|
+
confirmation_status=confirmation_status,
|
|
148
|
+
kickoff_sent=kickoff_sent,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return BootstrapCheckpointResult(
|
|
152
|
+
project_id=project_id,
|
|
153
|
+
mind_stage=mind_stage,
|
|
154
|
+
has_group_binding=has_group_binding,
|
|
155
|
+
has_project_card_doc=has_project_card_doc,
|
|
156
|
+
doc_gaps=doc_gaps,
|
|
157
|
+
alignment_ready=alignment_ready,
|
|
158
|
+
alignment_gaps=alignment_gaps,
|
|
159
|
+
milestones=BootstrapMilestones(
|
|
160
|
+
alignment_intro=intro_status,
|
|
161
|
+
execution_confirmation=confirmation_status,
|
|
162
|
+
kickoff=kickoff_status,
|
|
163
|
+
),
|
|
164
|
+
next_action=_select_next_action(
|
|
165
|
+
bootstrap_active=bootstrap_active,
|
|
166
|
+
has_group_binding=has_group_binding,
|
|
167
|
+
has_project_card_doc=has_project_card_doc,
|
|
168
|
+
doc_gaps=doc_gaps,
|
|
169
|
+
intro_status=intro_status,
|
|
170
|
+
alignment_ready=alignment_ready,
|
|
171
|
+
confirmation_status=confirmation_status,
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def mark_bootstrap_milestone(
|
|
177
|
+
*,
|
|
178
|
+
project_id: str,
|
|
179
|
+
milestone: str,
|
|
180
|
+
bootstrap_store: BootstrapReadStore,
|
|
181
|
+
pending_state_store: PendingStateStore,
|
|
182
|
+
) -> BootstrapMarkResult | None:
|
|
183
|
+
if milestone not in _MILESTONE_STATE_KINDS:
|
|
184
|
+
raise ValueError(f"unknown bootstrap milestone: {milestone}")
|
|
185
|
+
project = bootstrap_store.find_project_by_id(project_id)
|
|
186
|
+
if project is None:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
state_key = _STATE_KEY_BUILDERS[milestone](project_id)
|
|
190
|
+
already_marked = False
|
|
191
|
+
resolved = pending_state_store.resolve_open_state(
|
|
192
|
+
project_id=project_id, state_key=state_key
|
|
193
|
+
)
|
|
194
|
+
if resolved is None:
|
|
195
|
+
existing = pending_state_store.find_latest_by_state_key(
|
|
196
|
+
project_id=project_id, state_key=state_key
|
|
197
|
+
)
|
|
198
|
+
if existing is not None and existing.get("status") != "open":
|
|
199
|
+
already_marked = True
|
|
200
|
+
else:
|
|
201
|
+
# 没有任何登记也允许补账,保证"发过什么"永远可核验。
|
|
202
|
+
pending_state_store.upsert_open_state(
|
|
203
|
+
{
|
|
204
|
+
"project_id": project_id,
|
|
205
|
+
"scope_kind": "project",
|
|
206
|
+
"scope_ref": project_id,
|
|
207
|
+
"state_kind": _MILESTONE_STATE_KINDS[milestone],
|
|
208
|
+
"state_key": state_key,
|
|
209
|
+
"payload_json": {
|
|
210
|
+
"milestone": milestone,
|
|
211
|
+
"markedAt": _now_iso(),
|
|
212
|
+
},
|
|
213
|
+
"status": "resolved",
|
|
214
|
+
"expires_at": None,
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
checkpoint = evaluate_bootstrap_checkpoint(
|
|
219
|
+
project_id=project_id,
|
|
220
|
+
bootstrap_store=bootstrap_store,
|
|
221
|
+
pending_state_store=pending_state_store,
|
|
222
|
+
)
|
|
223
|
+
if checkpoint is None:
|
|
224
|
+
return None
|
|
225
|
+
return BootstrapMarkResult(
|
|
226
|
+
project_id=project_id,
|
|
227
|
+
milestone=milestone,
|
|
228
|
+
already_marked=already_marked,
|
|
229
|
+
checkpoint=checkpoint,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _build_alignment_gaps(
|
|
234
|
+
*, project: dict, versions: list[dict], tasks: list[dict]
|
|
235
|
+
) -> list[str]:
|
|
236
|
+
current_version = resolve_current_version(project=project, versions=versions)
|
|
237
|
+
if current_version is None:
|
|
238
|
+
return ["missing_current_version"]
|
|
239
|
+
current_tasks = [
|
|
240
|
+
task for task in tasks if task.get("version_id") == current_version.get("version_id")
|
|
241
|
+
]
|
|
242
|
+
gaps = [
|
|
243
|
+
f"current_version:{field}"
|
|
244
|
+
for field in describe_missing_version_fields(version=current_version, tasks=current_tasks)
|
|
245
|
+
]
|
|
246
|
+
if not current_tasks:
|
|
247
|
+
gaps.append("missing_trackable_task")
|
|
248
|
+
return gaps
|
|
249
|
+
for task in current_tasks:
|
|
250
|
+
for field in describe_missing_task_fields(task=task):
|
|
251
|
+
gaps.append(f"task:{task.get('task_id') or 'unknown'}:{field}")
|
|
252
|
+
return gaps
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _is_alignment_ready(*, project: dict, versions: list[dict], tasks: list[dict]) -> bool:
|
|
256
|
+
current_version = resolve_current_version(project=project, versions=versions)
|
|
257
|
+
if current_version is None:
|
|
258
|
+
return False
|
|
259
|
+
current_tasks = [
|
|
260
|
+
task for task in tasks if task.get("version_id") == current_version.get("version_id")
|
|
261
|
+
]
|
|
262
|
+
if describe_missing_version_fields(version=current_version, tasks=current_tasks):
|
|
263
|
+
return False
|
|
264
|
+
# 执行启动确认只要求至少一个可追踪 Task 必填字段齐全。
|
|
265
|
+
return any(not describe_missing_task_fields(task=task) for task in current_tasks)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _is_milestone_marked(
|
|
269
|
+
*, project_id: str, state_key: str, pending_state_store: PendingStateStore
|
|
270
|
+
) -> bool:
|
|
271
|
+
record = pending_state_store.find_latest_by_state_key(
|
|
272
|
+
project_id=project_id, state_key=state_key
|
|
273
|
+
)
|
|
274
|
+
return record is not None and record.get("status") != "open"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _reconcile_alignment_intro(
|
|
278
|
+
*,
|
|
279
|
+
project_id: str,
|
|
280
|
+
bootstrap_active: bool,
|
|
281
|
+
has_group_binding: bool,
|
|
282
|
+
pending_state_store: PendingStateStore,
|
|
283
|
+
) -> str:
|
|
284
|
+
if not bootstrap_active or not has_group_binding:
|
|
285
|
+
return "not_required"
|
|
286
|
+
state_key = alignment_intro_state_key(project_id)
|
|
287
|
+
record = pending_state_store.find_latest_by_state_key(
|
|
288
|
+
project_id=project_id, state_key=state_key
|
|
289
|
+
)
|
|
290
|
+
if record is None:
|
|
291
|
+
ensure_alignment_intro_pending(
|
|
292
|
+
project_id=project_id, pending_state_store=pending_state_store
|
|
293
|
+
)
|
|
294
|
+
return "pending"
|
|
295
|
+
if record.get("status") == "open":
|
|
296
|
+
return "pending"
|
|
297
|
+
return "sent"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _reconcile_execution_confirmation(
|
|
301
|
+
*,
|
|
302
|
+
project_id: str,
|
|
303
|
+
bootstrap_active: bool,
|
|
304
|
+
intro_status: str,
|
|
305
|
+
alignment_ready: bool,
|
|
306
|
+
pending_state_store: PendingStateStore,
|
|
307
|
+
) -> str:
|
|
308
|
+
if not bootstrap_active:
|
|
309
|
+
return "not_required"
|
|
310
|
+
state_key = execution_confirmation_state_key(project_id)
|
|
311
|
+
record = pending_state_store.find_latest_by_state_key(
|
|
312
|
+
project_id=project_id, state_key=state_key
|
|
313
|
+
)
|
|
314
|
+
if record is not None:
|
|
315
|
+
return "pending" if record.get("status") == "open" else "sent"
|
|
316
|
+
if intro_status != "sent" or not alignment_ready:
|
|
317
|
+
return "not_ready"
|
|
318
|
+
pending_state_store.upsert_open_state(
|
|
319
|
+
{
|
|
320
|
+
"project_id": project_id,
|
|
321
|
+
"scope_kind": "project",
|
|
322
|
+
"scope_ref": project_id,
|
|
323
|
+
"state_kind": _MILESTONE_STATE_KINDS[MILESTONE_EXECUTION_CONFIRMATION],
|
|
324
|
+
"state_key": state_key,
|
|
325
|
+
"payload_json": {
|
|
326
|
+
"milestone": MILESTONE_EXECUTION_CONFIRMATION,
|
|
327
|
+
"openedAt": _now_iso(),
|
|
328
|
+
},
|
|
329
|
+
"status": "open",
|
|
330
|
+
"expires_at": None,
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
return "pending"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _reconcile_kickoff(*, confirmation_status: str, kickoff_sent: bool) -> str:
|
|
337
|
+
if kickoff_sent:
|
|
338
|
+
return "sent"
|
|
339
|
+
if confirmation_status == "sent":
|
|
340
|
+
return "pending"
|
|
341
|
+
return "not_ready"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _select_next_action(
|
|
345
|
+
*,
|
|
346
|
+
bootstrap_active: bool,
|
|
347
|
+
has_group_binding: bool,
|
|
348
|
+
has_project_card_doc: bool,
|
|
349
|
+
doc_gaps: list[str],
|
|
350
|
+
intro_status: str,
|
|
351
|
+
alignment_ready: bool,
|
|
352
|
+
confirmation_status: str,
|
|
353
|
+
) -> str:
|
|
354
|
+
if not bootstrap_active:
|
|
355
|
+
return "bootstrap_complete"
|
|
356
|
+
if not has_group_binding:
|
|
357
|
+
return "create_group_binding"
|
|
358
|
+
# 对齐引导唯一硬前置是 Project Card;缺它先驱动文档落地。
|
|
359
|
+
if intro_status == "pending" and not has_project_card_doc:
|
|
360
|
+
return "land_project_card"
|
|
361
|
+
if intro_status == "pending":
|
|
362
|
+
return "send_alignment_intro"
|
|
363
|
+
if confirmation_status == "pending":
|
|
364
|
+
return "send_execution_confirmation"
|
|
365
|
+
# 消息欠账还清后,剩余文档欠账由 agent 当轮补齐,不阻塞人侧流程。
|
|
366
|
+
if doc_gaps:
|
|
367
|
+
return "backfill_docs"
|
|
368
|
+
if not alignment_ready:
|
|
369
|
+
return "collect_alignment_fields"
|
|
370
|
+
if confirmation_status == "sent":
|
|
371
|
+
return "await_owner_confirmation"
|
|
372
|
+
return "collect_alignment_fields"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _now_iso() -> str:
|
|
376
|
+
return pendulum.now("UTC").to_iso8601_string()
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from typing import Any, Protocol
|
|
2
|
+
|
|
3
|
+
from application.bootstrap.checkpoint import (
|
|
4
|
+
PendingStateStore,
|
|
5
|
+
ensure_alignment_intro_pending,
|
|
6
|
+
)
|
|
7
|
+
from application.context.binding_normalization import (
|
|
8
|
+
normalize_binding_channel,
|
|
9
|
+
normalize_external_channel_id_for_write,
|
|
10
|
+
)
|
|
11
|
+
from application.loop.enqueue import enqueue_bootstrap_continue
|
|
12
|
+
from domain.bootstrap.models import BootstrapBinding, BootstrapInitResult
|
|
13
|
+
from domain.shared.project_mind_derived import (
|
|
14
|
+
build_project_intro,
|
|
15
|
+
collect_participants,
|
|
16
|
+
resolve_current_version,
|
|
17
|
+
resolve_target_release_date,
|
|
18
|
+
)
|
|
19
|
+
from domain.shared.project_mind_readiness import (
|
|
20
|
+
build_group_provisioning_gaps,
|
|
21
|
+
build_v1_readiness_gaps,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BootstrapStore(Protocol):
|
|
26
|
+
def find_project_by_id(self, project_id: str) -> Any: ...
|
|
27
|
+
|
|
28
|
+
def upsert_project(self, payload: dict) -> Any: ...
|
|
29
|
+
|
|
30
|
+
def upsert_binding(self, payload: dict) -> Any: ...
|
|
31
|
+
|
|
32
|
+
def list_bindings_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
33
|
+
|
|
34
|
+
def list_versions_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
35
|
+
|
|
36
|
+
def list_tasks_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
37
|
+
|
|
38
|
+
def list_participants_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
39
|
+
|
|
40
|
+
def upsert_participant(self, payload: dict) -> Any: ...
|
|
41
|
+
|
|
42
|
+
def list_documents_by_project_id(self, project_id: str) -> list[Any]: ...
|
|
43
|
+
|
|
44
|
+
def find_monitoring_policy_by_project_id(self, project_id: str) -> Any: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def init_project_bootstrap(
|
|
48
|
+
*,
|
|
49
|
+
project_id: str,
|
|
50
|
+
project_name: str,
|
|
51
|
+
owner_user_id: str,
|
|
52
|
+
primary_goal: str,
|
|
53
|
+
target_release_date: str | None,
|
|
54
|
+
timezone: str | None,
|
|
55
|
+
channel: str | None,
|
|
56
|
+
project_group_id: str | None,
|
|
57
|
+
participant_user_ids: list[str] | None,
|
|
58
|
+
bootstrap_store: BootstrapStore,
|
|
59
|
+
pending_state_store: PendingStateStore | None = None,
|
|
60
|
+
loop_job_store: Any | None = None,
|
|
61
|
+
) -> BootstrapInitResult:
|
|
62
|
+
existing_project = bootstrap_store.find_project_by_id(project_id)
|
|
63
|
+
project = bootstrap_store.upsert_project(
|
|
64
|
+
{
|
|
65
|
+
"project_id": project_id,
|
|
66
|
+
"project_name": project_name,
|
|
67
|
+
"owner_user_id": owner_user_id,
|
|
68
|
+
"primary_goal": primary_goal or (existing_project or {}).get("primary_goal"),
|
|
69
|
+
"timezone": timezone,
|
|
70
|
+
"mind_stage": (existing_project or {}).get("mind_stage") or "v0",
|
|
71
|
+
"lifecycle_status": (existing_project or {}).get("lifecycle_status") or "active",
|
|
72
|
+
"current_revision_id": (existing_project or {}).get("current_revision_id"),
|
|
73
|
+
"current_version_id": (existing_project or {}).get("current_version_id"),
|
|
74
|
+
"target_release_date": target_release_date
|
|
75
|
+
if target_release_date is not None
|
|
76
|
+
else (existing_project or {}).get("target_release_date"),
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if project_group_id:
|
|
81
|
+
effective_channel = normalize_binding_channel(channel)
|
|
82
|
+
bootstrap_store.upsert_binding(
|
|
83
|
+
{
|
|
84
|
+
"project_id": project_id,
|
|
85
|
+
"channel": effective_channel,
|
|
86
|
+
"external_channel_id": normalize_external_channel_id_for_write(
|
|
87
|
+
channel=effective_channel,
|
|
88
|
+
external_channel_id=project_group_id,
|
|
89
|
+
),
|
|
90
|
+
"binding_kind": "project_group",
|
|
91
|
+
"status": "active",
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
# 群绑定是确定性写入点:在这里登记"欠对齐引导",不依赖 skill 自觉补账。
|
|
95
|
+
if pending_state_store is not None and project.get("mind_stage") == "v0":
|
|
96
|
+
ensure_alignment_intro_pending(
|
|
97
|
+
project_id=project_id,
|
|
98
|
+
pending_state_store=pending_state_store,
|
|
99
|
+
)
|
|
100
|
+
if loop_job_store is not None:
|
|
101
|
+
enqueue_bootstrap_continue(
|
|
102
|
+
project_id=project_id,
|
|
103
|
+
loop_job_store=loop_job_store,
|
|
104
|
+
reason="project_group_binding_ready",
|
|
105
|
+
source="bootstrap_init",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
bootstrap_store.upsert_participant(
|
|
109
|
+
{
|
|
110
|
+
"project_id": project_id,
|
|
111
|
+
"user_id": owner_user_id,
|
|
112
|
+
"role": "owner",
|
|
113
|
+
"status": "active",
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
for participant_user_id in _dedupe_participant_user_ids(participant_user_ids or []):
|
|
117
|
+
if participant_user_id == owner_user_id:
|
|
118
|
+
continue
|
|
119
|
+
bootstrap_store.upsert_participant(
|
|
120
|
+
{
|
|
121
|
+
"project_id": project_id,
|
|
122
|
+
"user_id": participant_user_id,
|
|
123
|
+
"role": "member",
|
|
124
|
+
"status": "active",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
bindings = [
|
|
129
|
+
BootstrapBinding(
|
|
130
|
+
binding_kind=binding["binding_kind"],
|
|
131
|
+
channel=binding["channel"],
|
|
132
|
+
external_channel_id=binding["external_channel_id"],
|
|
133
|
+
status=binding["status"],
|
|
134
|
+
)
|
|
135
|
+
for binding in bootstrap_store.list_bindings_by_project_id(project_id)
|
|
136
|
+
]
|
|
137
|
+
versions = bootstrap_store.list_versions_by_project_id(project_id)
|
|
138
|
+
tasks = bootstrap_store.list_tasks_by_project_id(project_id)
|
|
139
|
+
stored_participants = bootstrap_store.list_participants_by_project_id(project_id)
|
|
140
|
+
readiness_participants = [
|
|
141
|
+
*stored_participants,
|
|
142
|
+
*_participant_rows_from_project_mind(versions=versions, tasks=tasks),
|
|
143
|
+
]
|
|
144
|
+
documents = bootstrap_store.list_documents_by_project_id(project_id)
|
|
145
|
+
monitoring_policy = bootstrap_store.find_monitoring_policy_by_project_id(project_id)
|
|
146
|
+
group_provisioning_gaps = build_group_provisioning_gaps(
|
|
147
|
+
project=project,
|
|
148
|
+
participants=readiness_participants,
|
|
149
|
+
)
|
|
150
|
+
v1_gaps = build_v1_readiness_gaps(
|
|
151
|
+
project=project,
|
|
152
|
+
bindings=[binding.model_dump(mode="json", by_alias=False) for binding in bindings],
|
|
153
|
+
monitoring_enabled=bool((monitoring_policy or {}).get("enabled")),
|
|
154
|
+
documents=documents,
|
|
155
|
+
versions=versions,
|
|
156
|
+
tasks=tasks,
|
|
157
|
+
participants=readiness_participants,
|
|
158
|
+
)
|
|
159
|
+
current_version = resolve_current_version(project=project, versions=versions)
|
|
160
|
+
|
|
161
|
+
return BootstrapInitResult(
|
|
162
|
+
project_id=project["project_id"],
|
|
163
|
+
project_name=project["project_name"],
|
|
164
|
+
owner_user_id=project["owner_user_id"],
|
|
165
|
+
primary_goal=project.get("primary_goal") or "",
|
|
166
|
+
target_release_date=resolve_target_release_date(
|
|
167
|
+
project=project,
|
|
168
|
+
current_version=current_version,
|
|
169
|
+
),
|
|
170
|
+
participants=collect_participants(
|
|
171
|
+
versions=versions,
|
|
172
|
+
tasks=tasks,
|
|
173
|
+
participants=stored_participants,
|
|
174
|
+
),
|
|
175
|
+
project_intro=build_project_intro(project=project, versions=versions, tasks=tasks),
|
|
176
|
+
last_updated_at=project.get("last_updated_at"),
|
|
177
|
+
mind_stage=project["mind_stage"],
|
|
178
|
+
lifecycle_status=project["lifecycle_status"],
|
|
179
|
+
timezone=project.get("timezone"),
|
|
180
|
+
bindings=bindings,
|
|
181
|
+
group_provisioning_ready=not group_provisioning_gaps,
|
|
182
|
+
group_provisioning_gaps=group_provisioning_gaps,
|
|
183
|
+
v1_ready=not v1_gaps,
|
|
184
|
+
v1_gaps=v1_gaps,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _dedupe_participant_user_ids(values: list[str]) -> list[str]:
|
|
189
|
+
ordered: list[str] = []
|
|
190
|
+
for value in values:
|
|
191
|
+
normalized = value.strip()
|
|
192
|
+
if normalized and normalized not in ordered:
|
|
193
|
+
ordered.append(normalized)
|
|
194
|
+
return ordered
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _participant_rows_from_project_mind(*, versions: list[dict], tasks: list[dict]) -> list[dict]:
|
|
198
|
+
return [
|
|
199
|
+
{"user_id": user_id, "status": "active"}
|
|
200
|
+
for user_id in collect_participants(versions=versions, tasks=tasks)
|
|
201
|
+
]
|