devcouncil 0.1.0 → 0.1.1
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.
- package/LICENSE +201 -201
- package/README.md +62 -543
- package/package.json +1 -1
- package/pyproject.toml +29 -26
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +135 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +143 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- package/src/devcouncil/cli/commands/artifacts.py +51 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/config.py +76 -54
- package/src/devcouncil/cli/commands/dashboard.py +26 -0
- package/src/devcouncil/cli/commands/doctor.py +86 -42
- package/src/devcouncil/cli/commands/go.py +237 -0
- package/src/devcouncil/cli/commands/hook.py +96 -29
- package/src/devcouncil/cli/commands/init.py +67 -56
- package/src/devcouncil/cli/commands/integrate.py +320 -14
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +25 -21
- package/src/devcouncil/cli/commands/plan.py +257 -206
- package/src/devcouncil/cli/commands/prompt.py +36 -33
- package/src/devcouncil/cli/commands/repair.py +72 -69
- package/src/devcouncil/cli/commands/report.py +112 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
- package/src/devcouncil/cli/commands/rollback.py +49 -47
- package/src/devcouncil/cli/commands/run.py +252 -207
- package/src/devcouncil/cli/commands/setup.py +159 -18
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/status.py +117 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +2 -1
- package/src/devcouncil/cli/commands/verify.py +158 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/main.py +42 -24
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +27 -27
- package/src/devcouncil/domain/gap.py +26 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +26 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/hook_policy.py +24 -3
- package/src/devcouncil/execution/patch.py +28 -28
- package/src/devcouncil/execution/permissions.py +44 -44
- package/src/devcouncil/execution/prompt_builder.py +23 -23
- package/src/devcouncil/execution/task_runner.py +63 -63
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/coding_cli.py +112 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +81 -81
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +50 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
- package/src/devcouncil/gating/policy.py +157 -157
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +120 -0
- package/src/devcouncil/indexing/repo_mapper.py +208 -204
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/gitnexus.py +27 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +549 -96
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +207 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +138 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +38 -38
- package/src/devcouncil/llm/provider.py +146 -125
- package/src/devcouncil/llm/router.py +111 -111
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/critique_service.py +66 -66
- package/src/devcouncil/planning/plan_service.py +46 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +44 -44
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +20 -17
- package/src/devcouncil/reporting/markdown_report.py +68 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/storage/db.py +66 -66
- package/src/devcouncil/storage/models.py +83 -83
- package/src/devcouncil/storage/repositories.py +299 -222
- package/src/devcouncil/telemetry/cost.py +34 -34
- package/src/devcouncil/telemetry/tracker.py +49 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +122 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +141 -141
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/verifier.py +319 -302
- package/uv.lock +1 -1
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import json
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Optional, Any
|
|
5
|
-
|
|
6
|
-
from devcouncil.app.state_machine import StateMachine, ProjectPhase
|
|
7
|
-
from devcouncil.app.run_context import RunContext
|
|
8
|
-
from devcouncil.app.events import bus, EventTypes
|
|
1
|
+
import logging
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, Any
|
|
5
|
+
|
|
6
|
+
from devcouncil.app.state_machine import StateMachine, ProjectPhase
|
|
7
|
+
from devcouncil.app.run_context import RunContext
|
|
8
|
+
from devcouncil.app.events import bus, EventTypes
|
|
9
9
|
from devcouncil.storage.db import get_db
|
|
10
10
|
from devcouncil.storage.repositories import StateRepository
|
|
11
11
|
from devcouncil.telemetry.traces import TraceLogger
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
class Orchestrator:
|
|
16
|
-
"""Central orchestrator managing the workflow lifecycle and state transitions."""
|
|
17
|
-
def __init__(self, project_root: Path, persist_state: bool = True):
|
|
18
|
-
self.project_root = project_root
|
|
19
|
-
self.persist_state = persist_state
|
|
20
|
-
|
|
21
|
-
db = get_db(self.project_root)
|
|
22
|
-
if db:
|
|
23
|
-
with db.get_session() as session:
|
|
24
|
-
repo = StateRepository(session)
|
|
25
|
-
state = repo.get_state()
|
|
26
|
-
if state:
|
|
27
|
-
self.state_machine = StateMachine(ProjectPhase(state.current_phase))
|
|
28
|
-
self.state_machine._history = [ProjectPhase(p) for p in json.loads(state.history_json)]
|
|
29
|
-
else:
|
|
30
|
-
self.state_machine = StateMachine(ProjectPhase.NEW)
|
|
31
|
-
else:
|
|
32
|
-
self.state_machine = StateMachine(ProjectPhase.NEW)
|
|
33
|
-
|
|
34
|
-
self.current_run: Optional[RunContext] = None
|
|
35
|
-
|
|
36
|
-
def reset_state_machine(self, initial: ProjectPhase = ProjectPhase.NEW):
|
|
37
|
-
"""Start a new lifecycle sequence without depending on prior persisted phase."""
|
|
38
|
-
self.state_machine = StateMachine(initial)
|
|
39
|
-
|
|
40
|
-
async def start_run(self, run_id: str, goal: str) -> RunContext:
|
|
41
|
-
"""Start a new orchestration run."""
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
class Orchestrator:
|
|
16
|
+
"""Central orchestrator managing the workflow lifecycle and state transitions."""
|
|
17
|
+
def __init__(self, project_root: Path, persist_state: bool = True):
|
|
18
|
+
self.project_root = project_root
|
|
19
|
+
self.persist_state = persist_state
|
|
20
|
+
|
|
21
|
+
db = get_db(self.project_root)
|
|
22
|
+
if db:
|
|
23
|
+
with db.get_session() as session:
|
|
24
|
+
repo = StateRepository(session)
|
|
25
|
+
state = repo.get_state()
|
|
26
|
+
if state:
|
|
27
|
+
self.state_machine = StateMachine(ProjectPhase(state.current_phase))
|
|
28
|
+
self.state_machine._history = [ProjectPhase(p) for p in json.loads(state.history_json)]
|
|
29
|
+
else:
|
|
30
|
+
self.state_machine = StateMachine(ProjectPhase.NEW)
|
|
31
|
+
else:
|
|
32
|
+
self.state_machine = StateMachine(ProjectPhase.NEW)
|
|
33
|
+
|
|
34
|
+
self.current_run: Optional[RunContext] = None
|
|
35
|
+
|
|
36
|
+
def reset_state_machine(self, initial: ProjectPhase = ProjectPhase.NEW):
|
|
37
|
+
"""Start a new lifecycle sequence without depending on prior persisted phase."""
|
|
38
|
+
self.state_machine = StateMachine(initial)
|
|
39
|
+
|
|
40
|
+
async def start_run(self, run_id: str, goal: str) -> RunContext:
|
|
41
|
+
"""Start a new orchestration run."""
|
|
42
42
|
self.current_run = RunContext(
|
|
43
43
|
run_id=run_id,
|
|
44
44
|
project_root=str(self.project_root),
|
|
@@ -51,24 +51,24 @@ class Orchestrator:
|
|
|
51
51
|
run_id=run_id,
|
|
52
52
|
summary=f"Planning started: {goal}",
|
|
53
53
|
)
|
|
54
|
-
|
|
55
|
-
await bus.emit(EventTypes.PLANNING_STARTED, {"run_id": run_id, "goal": goal})
|
|
56
|
-
return self.current_run
|
|
57
|
-
|
|
58
|
-
async def transition_to(self, target_phase: ProjectPhase):
|
|
59
|
-
"""Transition the project phase and emit events."""
|
|
60
|
-
old_phase = self.state_machine.phase
|
|
61
|
-
self.state_machine.transition(target_phase)
|
|
62
|
-
|
|
63
|
-
db = get_db(self.project_root)
|
|
64
|
-
if db and self.persist_state:
|
|
65
|
-
with db.get_session() as session:
|
|
66
|
-
repo = StateRepository(session)
|
|
67
|
-
repo.save_state(
|
|
68
|
-
self.state_machine.phase.value,
|
|
69
|
-
[p.value for p in self.state_machine.history]
|
|
70
|
-
)
|
|
71
|
-
|
|
54
|
+
|
|
55
|
+
await bus.emit(EventTypes.PLANNING_STARTED, {"run_id": run_id, "goal": goal})
|
|
56
|
+
return self.current_run
|
|
57
|
+
|
|
58
|
+
async def transition_to(self, target_phase: ProjectPhase):
|
|
59
|
+
"""Transition the project phase and emit events."""
|
|
60
|
+
old_phase = self.state_machine.phase
|
|
61
|
+
self.state_machine.transition(target_phase)
|
|
62
|
+
|
|
63
|
+
db = get_db(self.project_root)
|
|
64
|
+
if db and self.persist_state:
|
|
65
|
+
with db.get_session() as session:
|
|
66
|
+
repo = StateRepository(session)
|
|
67
|
+
repo.save_state(
|
|
68
|
+
self.state_machine.phase.value,
|
|
69
|
+
[p.value for p in self.state_machine.history]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
72
|
logger.info(f"Transitioned from {old_phase.value} to {target_phase.value}")
|
|
73
73
|
TraceLogger(self.project_root).log_event(
|
|
74
74
|
"phase_transition",
|
|
@@ -79,14 +79,14 @@ class Orchestrator:
|
|
|
79
79
|
|
|
80
80
|
if target_phase == ProjectPhase.PLAN_APPROVED:
|
|
81
81
|
await bus.emit(EventTypes.PLANNING_COMPLETED, {"run_id": self.current_run.run_id if self.current_run else None})
|
|
82
|
-
|
|
83
|
-
def save_run_artifact(self, name: str, data: Any, is_json: bool = True):
|
|
84
|
-
"""Save a local artifact specific to this run (in .devcouncil/runs/)."""
|
|
85
|
-
if not self.current_run:
|
|
86
|
-
logger.warning("Attempted to save artifact without an active run context.")
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
if is_json:
|
|
90
|
-
self.current_run.save_json_artifact(name, data)
|
|
91
|
-
else:
|
|
92
|
-
self.current_run.save_artifact(name, data)
|
|
82
|
+
|
|
83
|
+
def save_run_artifact(self, name: str, data: Any, is_json: bool = True):
|
|
84
|
+
"""Save a local artifact specific to this run (in .devcouncil/runs/)."""
|
|
85
|
+
if not self.current_run:
|
|
86
|
+
logger.warning("Attempted to save artifact without an active run context.")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if is_json:
|
|
90
|
+
self.current_run.save_json_artifact(name, data)
|
|
91
|
+
else:
|
|
92
|
+
self.current_run.save_artifact(name, data)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from devcouncil.artifacts.graph import ArtifactGraph
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute_phase(graph: ArtifactGraph, persisted_phase: str | None = None) -> str:
|
|
7
|
+
"""Compute the current project phase, honoring explicit persisted state first."""
|
|
8
|
+
if persisted_phase:
|
|
9
|
+
return persisted_phase
|
|
10
|
+
|
|
11
|
+
reqs = list(graph.requirements.values())
|
|
12
|
+
tasks = list(graph.tasks.values())
|
|
13
|
+
blocking_gaps = graph.blocking_gaps()
|
|
14
|
+
if not reqs and not tasks:
|
|
15
|
+
return "NEW"
|
|
16
|
+
if reqs and not tasks:
|
|
17
|
+
return "REQUIREMENTS_DRAFTED"
|
|
18
|
+
if blocking_gaps:
|
|
19
|
+
return "TASK_BLOCKED"
|
|
20
|
+
if tasks:
|
|
21
|
+
statuses = {task.status for task in tasks}
|
|
22
|
+
if "running" in statuses:
|
|
23
|
+
return "TASK_EXECUTING"
|
|
24
|
+
if "blocked" in statuses:
|
|
25
|
+
return "TASK_BLOCKED"
|
|
26
|
+
if all(status in {"verified", "done"} for status in statuses):
|
|
27
|
+
return "PROJECT_DONE"
|
|
28
|
+
return "PLAN_APPROVED"
|
|
29
|
+
return "NEW"
|
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from datetime import datetime, timezone
|
|
3
|
-
from pydantic import BaseModel, Field
|
|
4
|
-
from typing import Optional, List, Dict, Any
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
|
-
|
|
10
|
-
class RunContext(BaseModel):
|
|
11
|
-
"""Context object encapsulating the current execution run."""
|
|
12
|
-
run_id: str
|
|
13
|
-
project_root: str
|
|
14
|
-
goal: Optional[str] = None
|
|
15
|
-
start_time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
16
|
-
active_tasks: List[str] = Field(default_factory=list)
|
|
17
|
-
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
18
|
-
|
|
19
|
-
@property
|
|
20
|
-
def run_dir(self) -> Path:
|
|
21
|
-
return Path(self.project_root) / ".devcouncil" / "runs" / self.run_id
|
|
22
|
-
|
|
23
|
-
def initialize(self):
|
|
24
|
-
"""Create the necessary run directory and sub-folders."""
|
|
25
|
-
self.run_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
-
logger.info(f"Initialized run context at {self.run_dir}")
|
|
27
|
-
|
|
28
|
-
def save_artifact(self, filename: str, content: str):
|
|
29
|
-
"""Save a text artifact associated with this run."""
|
|
30
|
-
path = self.run_dir / filename
|
|
31
|
-
path.write_text(content, encoding="utf-8")
|
|
32
|
-
logger.debug(f"Saved artifact to {path}")
|
|
33
|
-
|
|
34
|
-
def save_json_artifact(self, filename: str, data: Any):
|
|
35
|
-
"""Save a JSON artifact associated with this run."""
|
|
36
|
-
path = self.run_dir / filename
|
|
37
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
38
|
-
json.dump(data, f, indent=2)
|
|
39
|
-
logger.debug(f"Saved JSON artifact to {path}")
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class RunContext(BaseModel):
|
|
11
|
+
"""Context object encapsulating the current execution run."""
|
|
12
|
+
run_id: str
|
|
13
|
+
project_root: str
|
|
14
|
+
goal: Optional[str] = None
|
|
15
|
+
start_time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
16
|
+
active_tasks: List[str] = Field(default_factory=list)
|
|
17
|
+
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def run_dir(self) -> Path:
|
|
21
|
+
return Path(self.project_root) / ".devcouncil" / "runs" / self.run_id
|
|
22
|
+
|
|
23
|
+
def initialize(self):
|
|
24
|
+
"""Create the necessary run directory and sub-folders."""
|
|
25
|
+
self.run_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
logger.info(f"Initialized run context at {self.run_dir}")
|
|
27
|
+
|
|
28
|
+
def save_artifact(self, filename: str, content: str):
|
|
29
|
+
"""Save a text artifact associated with this run."""
|
|
30
|
+
path = self.run_dir / filename
|
|
31
|
+
path.write_text(content, encoding="utf-8")
|
|
32
|
+
logger.debug(f"Saved artifact to {path}")
|
|
33
|
+
|
|
34
|
+
def save_json_artifact(self, filename: str, data: Any):
|
|
35
|
+
"""Save a JSON artifact associated with this run."""
|
|
36
|
+
path = self.run_dir / filename
|
|
37
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
38
|
+
json.dump(data, f, indent=2)
|
|
39
|
+
logger.debug(f"Saved JSON artifact to {path}")
|
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
"""Gating state machine for DevCouncil project lifecycle.
|
|
2
|
-
|
|
3
|
-
States (from §12):
|
|
4
|
-
NEW -> REPO_MAPPED -> REQUIREMENTS_DRAFTED -> PLANS_GENERATED
|
|
5
|
-
-> CRITIQUES_GENERATED -> ARBITRATED -> AWAITING_USER_DECISIONS
|
|
6
|
-
-> PLAN_APPROVED -> TASK_READY -> TASK_EXECUTING -> TASK_VERIFYING
|
|
7
|
-
-> TASK_BLOCKED | TASK_VERIFIED -> PROJECT_DONE
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from enum import Enum
|
|
13
|
-
from typing import Dict, List, Set
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ProjectPhase(str, Enum):
|
|
18
|
-
NEW = "NEW"
|
|
19
|
-
REPO_MAPPED = "REPO_MAPPED"
|
|
20
|
-
REQUIREMENTS_DRAFTED = "REQUIREMENTS_DRAFTED"
|
|
21
|
-
PLANS_GENERATED = "PLANS_GENERATED"
|
|
22
|
-
CRITIQUES_GENERATED = "CRITIQUES_GENERATED"
|
|
23
|
-
ARBITRATED = "ARBITRATED"
|
|
24
|
-
AWAITING_USER_DECISIONS = "AWAITING_USER_DECISIONS"
|
|
25
|
-
PLAN_APPROVED = "PLAN_APPROVED"
|
|
26
|
-
TASK_READY = "TASK_READY"
|
|
27
|
-
TASK_EXECUTING = "TASK_EXECUTING"
|
|
28
|
-
TASK_VERIFYING = "TASK_VERIFYING"
|
|
29
|
-
TASK_BLOCKED = "TASK_BLOCKED"
|
|
30
|
-
TASK_VERIFIED = "TASK_VERIFIED"
|
|
31
|
-
PROJECT_DONE = "PROJECT_DONE"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# Valid transitions: from_phase -> set of valid next phases
|
|
35
|
-
TRANSITIONS: Dict[ProjectPhase, Set[ProjectPhase]] = {
|
|
36
|
-
ProjectPhase.NEW: {ProjectPhase.REPO_MAPPED},
|
|
37
|
-
ProjectPhase.REPO_MAPPED: {ProjectPhase.REQUIREMENTS_DRAFTED},
|
|
38
|
-
ProjectPhase.REQUIREMENTS_DRAFTED: {ProjectPhase.PLANS_GENERATED},
|
|
39
|
-
ProjectPhase.PLANS_GENERATED: {ProjectPhase.CRITIQUES_GENERATED},
|
|
40
|
-
ProjectPhase.CRITIQUES_GENERATED: {ProjectPhase.ARBITRATED},
|
|
41
|
-
ProjectPhase.ARBITRATED: {
|
|
42
|
-
ProjectPhase.AWAITING_USER_DECISIONS,
|
|
43
|
-
ProjectPhase.PLAN_APPROVED,
|
|
44
|
-
},
|
|
45
|
-
ProjectPhase.AWAITING_USER_DECISIONS: {ProjectPhase.PLAN_APPROVED},
|
|
46
|
-
ProjectPhase.PLAN_APPROVED: {ProjectPhase.TASK_READY},
|
|
47
|
-
ProjectPhase.TASK_READY: {ProjectPhase.TASK_EXECUTING},
|
|
48
|
-
ProjectPhase.TASK_EXECUTING: {ProjectPhase.TASK_VERIFYING},
|
|
49
|
-
ProjectPhase.TASK_VERIFYING: {
|
|
50
|
-
ProjectPhase.TASK_VERIFIED,
|
|
51
|
-
ProjectPhase.TASK_BLOCKED,
|
|
52
|
-
},
|
|
53
|
-
ProjectPhase.TASK_BLOCKED: {
|
|
54
|
-
ProjectPhase.TASK_READY, # after repair
|
|
55
|
-
},
|
|
56
|
-
ProjectPhase.TASK_VERIFIED: {
|
|
57
|
-
ProjectPhase.TASK_READY, # next task
|
|
58
|
-
ProjectPhase.PROJECT_DONE,
|
|
59
|
-
},
|
|
60
|
-
ProjectPhase.PROJECT_DONE: set(),
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
class InvalidTransitionError(Exception):
|
|
65
|
-
"""Raised when a state transition is not allowed."""
|
|
66
|
-
|
|
67
|
-
def __init__(self, current: ProjectPhase, target: ProjectPhase):
|
|
68
|
-
self.current = current
|
|
69
|
-
self.target = target
|
|
70
|
-
super().__init__(
|
|
71
|
-
f"Invalid transition: {current.value} -> {target.value}. "
|
|
72
|
-
f"Valid targets: {', '.join(t.value for t in TRANSITIONS.get(current, set()))}"
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class StateMachine:
|
|
77
|
-
"""Manages project phase transitions with validation."""
|
|
78
|
-
|
|
79
|
-
def __init__(self, initial: ProjectPhase = ProjectPhase.NEW):
|
|
80
|
-
self._phase = initial
|
|
81
|
-
self._history: List[ProjectPhase] = [initial]
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def phase(self) -> ProjectPhase:
|
|
85
|
-
return self._phase
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
def history(self) -> List[ProjectPhase]:
|
|
89
|
-
return list(self._history)
|
|
90
|
-
|
|
91
|
-
def can_transition(self, target: ProjectPhase) -> bool:
|
|
92
|
-
"""Check if a transition to the target phase is valid."""
|
|
93
|
-
valid = TRANSITIONS.get(self._phase, set())
|
|
94
|
-
return target in valid
|
|
95
|
-
|
|
96
|
-
def transition(self, target: ProjectPhase) -> None:
|
|
97
|
-
"""Transition to a new phase.
|
|
98
|
-
|
|
99
|
-
Raises InvalidTransitionError if the transition is not allowed.
|
|
100
|
-
"""
|
|
101
|
-
if not self.can_transition(target):
|
|
102
|
-
raise InvalidTransitionError(self._phase, target)
|
|
103
|
-
self._phase = target
|
|
104
|
-
self._history.append(target)
|
|
105
|
-
|
|
106
|
-
def valid_transitions(self) -> Set[ProjectPhase]:
|
|
107
|
-
"""Return the set of valid next phases from the current state."""
|
|
108
|
-
return TRANSITIONS.get(self._phase, set())
|
|
1
|
+
"""Gating state machine for DevCouncil project lifecycle.
|
|
2
|
+
|
|
3
|
+
States (from §12):
|
|
4
|
+
NEW -> REPO_MAPPED -> REQUIREMENTS_DRAFTED -> PLANS_GENERATED
|
|
5
|
+
-> CRITIQUES_GENERATED -> ARBITRATED -> AWAITING_USER_DECISIONS
|
|
6
|
+
-> PLAN_APPROVED -> TASK_READY -> TASK_EXECUTING -> TASK_VERIFYING
|
|
7
|
+
-> TASK_BLOCKED | TASK_VERIFIED -> PROJECT_DONE
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Dict, List, Set
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProjectPhase(str, Enum):
|
|
18
|
+
NEW = "NEW"
|
|
19
|
+
REPO_MAPPED = "REPO_MAPPED"
|
|
20
|
+
REQUIREMENTS_DRAFTED = "REQUIREMENTS_DRAFTED"
|
|
21
|
+
PLANS_GENERATED = "PLANS_GENERATED"
|
|
22
|
+
CRITIQUES_GENERATED = "CRITIQUES_GENERATED"
|
|
23
|
+
ARBITRATED = "ARBITRATED"
|
|
24
|
+
AWAITING_USER_DECISIONS = "AWAITING_USER_DECISIONS"
|
|
25
|
+
PLAN_APPROVED = "PLAN_APPROVED"
|
|
26
|
+
TASK_READY = "TASK_READY"
|
|
27
|
+
TASK_EXECUTING = "TASK_EXECUTING"
|
|
28
|
+
TASK_VERIFYING = "TASK_VERIFYING"
|
|
29
|
+
TASK_BLOCKED = "TASK_BLOCKED"
|
|
30
|
+
TASK_VERIFIED = "TASK_VERIFIED"
|
|
31
|
+
PROJECT_DONE = "PROJECT_DONE"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Valid transitions: from_phase -> set of valid next phases
|
|
35
|
+
TRANSITIONS: Dict[ProjectPhase, Set[ProjectPhase]] = {
|
|
36
|
+
ProjectPhase.NEW: {ProjectPhase.REPO_MAPPED},
|
|
37
|
+
ProjectPhase.REPO_MAPPED: {ProjectPhase.REQUIREMENTS_DRAFTED},
|
|
38
|
+
ProjectPhase.REQUIREMENTS_DRAFTED: {ProjectPhase.PLANS_GENERATED},
|
|
39
|
+
ProjectPhase.PLANS_GENERATED: {ProjectPhase.CRITIQUES_GENERATED},
|
|
40
|
+
ProjectPhase.CRITIQUES_GENERATED: {ProjectPhase.ARBITRATED},
|
|
41
|
+
ProjectPhase.ARBITRATED: {
|
|
42
|
+
ProjectPhase.AWAITING_USER_DECISIONS,
|
|
43
|
+
ProjectPhase.PLAN_APPROVED,
|
|
44
|
+
},
|
|
45
|
+
ProjectPhase.AWAITING_USER_DECISIONS: {ProjectPhase.PLAN_APPROVED},
|
|
46
|
+
ProjectPhase.PLAN_APPROVED: {ProjectPhase.TASK_READY},
|
|
47
|
+
ProjectPhase.TASK_READY: {ProjectPhase.TASK_EXECUTING},
|
|
48
|
+
ProjectPhase.TASK_EXECUTING: {ProjectPhase.TASK_VERIFYING},
|
|
49
|
+
ProjectPhase.TASK_VERIFYING: {
|
|
50
|
+
ProjectPhase.TASK_VERIFIED,
|
|
51
|
+
ProjectPhase.TASK_BLOCKED,
|
|
52
|
+
},
|
|
53
|
+
ProjectPhase.TASK_BLOCKED: {
|
|
54
|
+
ProjectPhase.TASK_READY, # after repair
|
|
55
|
+
},
|
|
56
|
+
ProjectPhase.TASK_VERIFIED: {
|
|
57
|
+
ProjectPhase.TASK_READY, # next task
|
|
58
|
+
ProjectPhase.PROJECT_DONE,
|
|
59
|
+
},
|
|
60
|
+
ProjectPhase.PROJECT_DONE: set(),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class InvalidTransitionError(Exception):
|
|
65
|
+
"""Raised when a state transition is not allowed."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, current: ProjectPhase, target: ProjectPhase):
|
|
68
|
+
self.current = current
|
|
69
|
+
self.target = target
|
|
70
|
+
super().__init__(
|
|
71
|
+
f"Invalid transition: {current.value} -> {target.value}. "
|
|
72
|
+
f"Valid targets: {', '.join(t.value for t in TRANSITIONS.get(current, set()))}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class StateMachine:
|
|
77
|
+
"""Manages project phase transitions with validation."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, initial: ProjectPhase = ProjectPhase.NEW):
|
|
80
|
+
self._phase = initial
|
|
81
|
+
self._history: List[ProjectPhase] = [initial]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def phase(self) -> ProjectPhase:
|
|
85
|
+
return self._phase
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def history(self) -> List[ProjectPhase]:
|
|
89
|
+
return list(self._history)
|
|
90
|
+
|
|
91
|
+
def can_transition(self, target: ProjectPhase) -> bool:
|
|
92
|
+
"""Check if a transition to the target phase is valid."""
|
|
93
|
+
valid = TRANSITIONS.get(self._phase, set())
|
|
94
|
+
return target in valid
|
|
95
|
+
|
|
96
|
+
def transition(self, target: ProjectPhase) -> None:
|
|
97
|
+
"""Transition to a new phase.
|
|
98
|
+
|
|
99
|
+
Raises InvalidTransitionError if the transition is not allowed.
|
|
100
|
+
"""
|
|
101
|
+
if not self.can_transition(target):
|
|
102
|
+
raise InvalidTransitionError(self._phase, target)
|
|
103
|
+
self._phase = target
|
|
104
|
+
self._history.append(target)
|
|
105
|
+
|
|
106
|
+
def valid_transitions(self) -> Set[ProjectPhase]:
|
|
107
|
+
"""Return the set of valid next phases from the current state."""
|
|
108
|
+
return TRANSITIONS.get(self._phase, set())
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"""Artifact package: graph, coverage, and validation for the persistent artifact DAG."""
|
|
1
|
+
"""Artifact package: graph, coverage, and validation for the persistent artifact DAG."""
|