devcouncil 0.1.1 → 0.2.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.
Files changed (146) hide show
  1. package/README.md +218 -96
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +7 -11
  4. package/pyproject.toml +0 -34
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -158
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -143
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/cli/__init__.py +0 -0
  23. package/src/devcouncil/cli/commands/__init__.py +0 -0
  24. package/src/devcouncil/cli/commands/artifacts.py +0 -51
  25. package/src/devcouncil/cli/commands/ast.py +0 -22
  26. package/src/devcouncil/cli/commands/baseline.py +0 -35
  27. package/src/devcouncil/cli/commands/config.py +0 -76
  28. package/src/devcouncil/cli/commands/dashboard.py +0 -26
  29. package/src/devcouncil/cli/commands/doctor.py +0 -140
  30. package/src/devcouncil/cli/commands/go.py +0 -237
  31. package/src/devcouncil/cli/commands/hook.py +0 -128
  32. package/src/devcouncil/cli/commands/init.py +0 -153
  33. package/src/devcouncil/cli/commands/integrate.py +0 -726
  34. package/src/devcouncil/cli/commands/lsp.py +0 -20
  35. package/src/devcouncil/cli/commands/map.py +0 -42
  36. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  37. package/src/devcouncil/cli/commands/plan.py +0 -327
  38. package/src/devcouncil/cli/commands/prompt.py +0 -50
  39. package/src/devcouncil/cli/commands/repair.py +0 -72
  40. package/src/devcouncil/cli/commands/report.py +0 -129
  41. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
  42. package/src/devcouncil/cli/commands/rollback.py +0 -60
  43. package/src/devcouncil/cli/commands/run.py +0 -269
  44. package/src/devcouncil/cli/commands/setup.py +0 -223
  45. package/src/devcouncil/cli/commands/show.py +0 -76
  46. package/src/devcouncil/cli/commands/status.py +0 -117
  47. package/src/devcouncil/cli/commands/tasks.py +0 -55
  48. package/src/devcouncil/cli/commands/trace.py +0 -44
  49. package/src/devcouncil/cli/commands/verify.py +0 -193
  50. package/src/devcouncil/cli/commands/version.py +0 -20
  51. package/src/devcouncil/cli/commands/watch.py +0 -574
  52. package/src/devcouncil/cli/main.py +0 -88
  53. package/src/devcouncil/council/__init__.py +0 -0
  54. package/src/devcouncil/council/prompts/__init__.py +0 -0
  55. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  56. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  57. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  58. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  59. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  60. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  61. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  62. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  63. package/src/devcouncil/domain/__init__.py +0 -0
  64. package/src/devcouncil/domain/assumption.py +0 -17
  65. package/src/devcouncil/domain/critique.py +0 -32
  66. package/src/devcouncil/domain/evidence.py +0 -27
  67. package/src/devcouncil/domain/gap.py +0 -26
  68. package/src/devcouncil/domain/requirement.py +0 -22
  69. package/src/devcouncil/domain/task.py +0 -26
  70. package/src/devcouncil/execution/__init__.py +0 -1
  71. package/src/devcouncil/execution/context_builder.py +0 -60
  72. package/src/devcouncil/execution/executor.py +0 -15
  73. package/src/devcouncil/execution/hook_policy.py +0 -165
  74. package/src/devcouncil/execution/patch.py +0 -28
  75. package/src/devcouncil/execution/paths.py +0 -14
  76. package/src/devcouncil/execution/permissions.py +0 -92
  77. package/src/devcouncil/execution/prompt_builder.py +0 -59
  78. package/src/devcouncil/execution/task_runner.py +0 -166
  79. package/src/devcouncil/executors/__init__.py +0 -1
  80. package/src/devcouncil/executors/coding_cli.py +0 -112
  81. package/src/devcouncil/executors/mini_swe.py +0 -73
  82. package/src/devcouncil/executors/native/__init__.py +0 -0
  83. package/src/devcouncil/executors/native/agent.py +0 -107
  84. package/src/devcouncil/executors/openhands.py +0 -71
  85. package/src/devcouncil/gating/__init__.py +0 -1
  86. package/src/devcouncil/gating/checks/__init__.py +0 -0
  87. package/src/devcouncil/gating/checks/clean_git.py +0 -50
  88. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  89. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  90. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
  91. package/src/devcouncil/gating/policy.py +0 -190
  92. package/src/devcouncil/indexing/__init__.py +0 -1
  93. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  94. package/src/devcouncil/indexing/graph_index.py +0 -48
  95. package/src/devcouncil/indexing/lsp.py +0 -120
  96. package/src/devcouncil/indexing/repo_mapper.py +0 -208
  97. package/src/devcouncil/indexing/symbol_index.py +0 -0
  98. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  99. package/src/devcouncil/integrations/github.py +0 -39
  100. package/src/devcouncil/integrations/gitnexus.py +0 -27
  101. package/src/devcouncil/integrations/graphify.py +0 -34
  102. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  103. package/src/devcouncil/integrations/mcp/server.py +0 -599
  104. package/src/devcouncil/integrations/pr_comments.py +0 -62
  105. package/src/devcouncil/live/__init__.py +0 -2
  106. package/src/devcouncil/live/cards.py +0 -207
  107. package/src/devcouncil/live/models.py +0 -63
  108. package/src/devcouncil/live/repair_prompt.py +0 -83
  109. package/src/devcouncil/live/reviewer.py +0 -70
  110. package/src/devcouncil/live/signals.py +0 -135
  111. package/src/devcouncil/live/summary.py +0 -34
  112. package/src/devcouncil/live/tasks.py +0 -18
  113. package/src/devcouncil/live/transcripts.py +0 -138
  114. package/src/devcouncil/llm/__init__.py +0 -1
  115. package/src/devcouncil/llm/cache.py +0 -38
  116. package/src/devcouncil/llm/provider.py +0 -146
  117. package/src/devcouncil/llm/router.py +0 -125
  118. package/src/devcouncil/planning/__init__.py +0 -1
  119. package/src/devcouncil/planning/arbiter_service.py +0 -57
  120. package/src/devcouncil/planning/critique_service.py +0 -66
  121. package/src/devcouncil/planning/plan_service.py +0 -46
  122. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
  123. package/src/devcouncil/planning/repair_service.py +0 -39
  124. package/src/devcouncil/planning/spec_service.py +0 -44
  125. package/src/devcouncil/repo/__init__.py +0 -0
  126. package/src/devcouncil/reporting/__init__.py +0 -0
  127. package/src/devcouncil/reporting/github_check.py +0 -32
  128. package/src/devcouncil/reporting/json_report.py +0 -20
  129. package/src/devcouncil/reporting/markdown_report.py +0 -68
  130. package/src/devcouncil/reporting/report_builder.py +0 -14
  131. package/src/devcouncil/storage/__init__.py +0 -0
  132. package/src/devcouncil/storage/db.py +0 -66
  133. package/src/devcouncil/storage/models.py +0 -83
  134. package/src/devcouncil/storage/repositories.py +0 -423
  135. package/src/devcouncil/telemetry/__init__.py +0 -0
  136. package/src/devcouncil/telemetry/cost.py +0 -34
  137. package/src/devcouncil/telemetry/traces.py +0 -91
  138. package/src/devcouncil/telemetry/tracker.py +0 -49
  139. package/src/devcouncil/ui/__init__.py +0 -1
  140. package/src/devcouncil/ui/dashboard.py +0 -122
  141. package/src/devcouncil/utils/__init__.py +0 -1
  142. package/src/devcouncil/utils/redaction.py +0 -141
  143. package/src/devcouncil/verification/__init__.py +0 -1
  144. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  145. package/src/devcouncil/verification/verifier.py +0 -530
  146. package/uv.lock +0 -1085
@@ -1,28 +0,0 @@
1
- from devcouncil.app.state_machine import StateMachine, ProjectPhase
2
- from devcouncil.app.run_context import RunContext
3
- from devcouncil.app.orchestrator import Orchestrator
4
- from devcouncil.app.events import EventBus, bus, EventTypes
5
- from devcouncil.app.errors import (
6
- DevCouncilError,
7
- GatingError,
8
- ConfigurationError,
9
- OrchestrationError,
10
- ExecutionError,
11
- VerificationError,
12
- )
13
-
14
- __all__ = [
15
- "StateMachine",
16
- "ProjectPhase",
17
- "RunContext",
18
- "Orchestrator",
19
- "EventBus",
20
- "bus",
21
- "EventTypes",
22
- "DevCouncilError",
23
- "GatingError",
24
- "ConfigurationError",
25
- "OrchestrationError",
26
- "ExecutionError",
27
- "VerificationError",
28
- ]
@@ -1,158 +0,0 @@
1
- """Typed configuration loader with Pydantic validation.
2
-
3
- Replaces scattered yaml.safe_load() calls with a single validated config service.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import os
9
- from pathlib import Path
10
- from typing import Dict, List
11
-
12
- from pydantic import BaseModel, Field
13
-
14
-
15
- class ModelRoleConfig(BaseModel):
16
- model: str
17
- temperature: float = 0.0
18
-
19
-
20
- class ModelsConfig(BaseModel):
21
- provider: str = "openrouter"
22
- roles: Dict[str, ModelRoleConfig] = Field(default_factory=dict)
23
-
24
-
25
- class ProjectConfig(BaseModel):
26
- name: str = "devcouncil-project"
27
- root: str = "."
28
- default_branch: str = "main"
29
-
30
-
31
- class CommandsConfig(BaseModel):
32
- test: List[str] = Field(default_factory=list)
33
- lint: List[str] = Field(default_factory=list)
34
- typecheck: List[str] = Field(default_factory=list)
35
-
36
-
37
- class GatesConfig(BaseModel):
38
- require_clean_git_before_task: bool = True
39
- block_orphan_diffs: bool = True
40
- block_missing_tests_for_high_requirements: bool = True
41
- block_dependency_changes_without_approval: bool = True
42
- block_schema_change_without_migration: bool = True
43
- block_failed_commands: bool = True
44
-
45
-
46
- class ExecutionConfig(BaseModel):
47
- default_executor: str = "manual"
48
- max_repair_attempts: int = 3
49
- checkpoint_before_each_task: bool = True
50
- command_timeout: int = 300
51
-
52
-
53
- class PrivacyConfig(BaseModel):
54
- redact_env_vars: bool = True
55
- redact_secrets_in_logs: bool = True
56
- store_prompts_locally: bool = True
57
-
58
-
59
- class AgentFlowIntegrationConfig(BaseModel):
60
- enabled: bool = False
61
- trace_path: str = ".devcouncil/logs/traces.jsonl"
62
- mode: str = "jsonl"
63
-
64
-
65
- class CodeReviewGraphIntegrationConfig(BaseModel):
66
- enabled: bool = False
67
- command: str = "code-review-graph"
68
- optional: bool = True
69
-
70
-
71
- class LiveReviewIntegrationConfig(BaseModel):
72
- enabled: bool = True
73
- cards_path: str = ".devcouncil/live/cards"
74
- signals_path: str = ".devcouncil/live/signals"
75
- default_client: str = "claude"
76
-
77
-
78
- class IntegrationsConfig(BaseModel):
79
- agent_flow: AgentFlowIntegrationConfig = Field(default_factory=AgentFlowIntegrationConfig)
80
- code_review_graph: CodeReviewGraphIntegrationConfig = Field(default_factory=CodeReviewGraphIntegrationConfig)
81
- live_review: LiveReviewIntegrationConfig = Field(default_factory=LiveReviewIntegrationConfig)
82
-
83
-
84
- class ProviderConfig(BaseModel):
85
- sort: str = "price"
86
- allow_fallbacks: bool = True
87
- require_parameters: bool = True
88
- data_collection: str = "deny"
89
-
90
-
91
- class DevCouncilConfig(BaseModel):
92
- """Top-level validated configuration."""
93
-
94
- project: ProjectConfig = Field(default_factory=ProjectConfig)
95
- models: ModelsConfig = Field(default_factory=ModelsConfig)
96
- provider: ProviderConfig = Field(default_factory=ProviderConfig)
97
- commands: CommandsConfig = Field(default_factory=CommandsConfig)
98
- gates: GatesConfig = Field(default_factory=GatesConfig)
99
- execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
100
- privacy: PrivacyConfig = Field(default_factory=PrivacyConfig)
101
- integrations: IntegrationsConfig = Field(default_factory=IntegrationsConfig)
102
-
103
-
104
- def load_config(project_root: Path = Path(".")) -> DevCouncilConfig:
105
- """Load and validate .devcouncil/config.yaml.
106
-
107
- Returns DevCouncilConfig with defaults for any missing fields.
108
- Raises FileNotFoundError if config doesn't exist.
109
- """
110
- import yaml
111
-
112
- config_path = project_root / ".devcouncil" / "config.yaml"
113
- if not config_path.exists():
114
- raise FileNotFoundError(f"Config not found at {config_path}. Run 'dev init' first.")
115
-
116
- with open(config_path) as f:
117
- raw = yaml.safe_load(f) or {}
118
-
119
- return DevCouncilConfig.model_validate(raw)
120
-
121
-
122
- def provider_api_key_env_var(provider: str = "openrouter") -> str:
123
- env_map = {
124
- "openrouter": "OPENROUTER_API_KEY",
125
- "openai": "OPENAI_API_KEY",
126
- "anthropic": "ANTHROPIC_API_KEY",
127
- }
128
- return env_map.get(provider, f"{provider.upper()}_API_KEY")
129
-
130
-
131
- def load_local_secrets(project_root: Path = Path(".")) -> Dict[str, str]:
132
- secrets_path = project_root / ".devcouncil" / "secrets.env"
133
- if not secrets_path.exists():
134
- return {}
135
-
136
- secrets: Dict[str, str] = {}
137
- for line in secrets_path.read_text(encoding="utf-8").splitlines():
138
- stripped = line.strip()
139
- if not stripped or stripped.startswith("#") or "=" not in stripped:
140
- continue
141
- key, value = stripped.split("=", 1)
142
- secrets[key.strip()] = value.strip().strip('"').strip("'")
143
- return secrets
144
-
145
-
146
- def get_api_key(provider: str = "openrouter", project_root: Path = Path(".")) -> str:
147
- """Retrieve the API key for the configured provider from environment.
148
-
149
- Raises ValueError if not found.
150
- """
151
- env_var = provider_api_key_env_var(provider)
152
- key = os.environ.get(env_var) or load_local_secrets(project_root).get(env_var)
153
- if not key:
154
- raise ValueError(
155
- f"API key not found. Set {env_var} in your environment or run 'dev setup'. "
156
- f"Provider: {provider}"
157
- )
158
- return key
@@ -1,23 +0,0 @@
1
- class DevCouncilError(Exception):
2
- """Base exception for all DevCouncil errors."""
3
- pass
4
-
5
- class GatingError(DevCouncilError):
6
- """Raised when a task or plan fails a mandatory gate."""
7
- pass
8
-
9
- class ConfigurationError(DevCouncilError):
10
- """Raised when the project configuration is invalid or missing."""
11
- pass
12
-
13
- class OrchestrationError(DevCouncilError):
14
- """Raised when the orchestrator encounters an unexpected state."""
15
- pass
16
-
17
- class ExecutionError(DevCouncilError):
18
- """Raised when an external or internal executor fails to run a task."""
19
- pass
20
-
21
- class VerificationError(DevCouncilError):
22
- """Raised when the verifier fails to run its checks properly."""
23
- pass
@@ -1,44 +0,0 @@
1
- from typing import Any, Callable, Dict, List
2
- import inspect
3
- import logging
4
-
5
- logger = logging.getLogger(__name__)
6
-
7
- class EventBus:
8
- """Simple asynchronous event bus for DevCouncil orchestration."""
9
- def __init__(self):
10
- self._listeners: Dict[str, List[Callable]] = {}
11
-
12
- def subscribe(self, event_type: str, callback: Callable):
13
- if event_type not in self._listeners:
14
- self._listeners[event_type] = []
15
- self._listeners[event_type].append(callback)
16
-
17
- async def emit(self, event_type: str, payload: Any = None):
18
- """Emit an event asynchronously to all registered listeners.
19
-
20
- Supports both sync and async callbacks.
21
- """
22
- logger.debug(f"Event emitted: {event_type}")
23
- if event_type in self._listeners:
24
- for callback in self._listeners[event_type]:
25
- try:
26
- if inspect.iscoroutinefunction(callback):
27
- await callback(payload)
28
- else:
29
- callback(payload)
30
- except Exception as e:
31
- logger.error(f"Error in event listener for {event_type}: {e}")
32
-
33
- # Global event bus instance
34
- bus = EventBus()
35
-
36
- # Standard Event Types
37
- class EventTypes:
38
- PLANNING_STARTED = "planning_started"
39
- PLANNING_COMPLETED = "planning_completed"
40
- TASK_EXECUTING = "task_executing"
41
- TASK_VERIFIED = "task_verified"
42
- TASK_BLOCKED = "task_blocked"
43
- GATE_FAILED = "gate_failed"
44
- MODEL_CALLED = "model_called"
@@ -1,92 +0,0 @@
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
- from devcouncil.storage.db import get_db
10
- from devcouncil.storage.repositories import StateRepository
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."""
42
- self.current_run = RunContext(
43
- run_id=run_id,
44
- project_root=str(self.project_root),
45
- goal=goal
46
- )
47
- self.current_run.initialize()
48
- TraceLogger(self.project_root).log_event(
49
- "planning_started",
50
- {"goal": goal},
51
- run_id=run_id,
52
- summary=f"Planning started: {goal}",
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
-
72
- logger.info(f"Transitioned from {old_phase.value} to {target_phase.value}")
73
- TraceLogger(self.project_root).log_event(
74
- "phase_transition",
75
- {"from": old_phase.value, "to": target_phase.value},
76
- run_id=self.current_run.run_id if self.current_run else None,
77
- summary=f"{old_phase.value} -> {target_phase.value}",
78
- )
79
-
80
- if target_phase == ProjectPhase.PLAN_APPROVED:
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)
@@ -1,29 +0,0 @@
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 +0,0 @@
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 +0,0 @@
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 +0,0 @@
1
- """Artifact package: graph, coverage, and validation for the persistent artifact DAG."""
@@ -1,96 +0,0 @@
1
- """Coverage analysis utilities for the artifact graph."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Dict, List, Tuple
6
-
7
- from devcouncil.artifacts.graph import ArtifactGraph
8
-
9
-
10
- def requirement_task_matrix(graph: ArtifactGraph) -> List[Dict[str, str]]:
11
- """Build a requirement → task → status coverage matrix for reporting.
12
-
13
- Returns a list of dicts suitable for rendering as a table:
14
- [{"requirement": "REQ-001", "title": "...", "task": "TASK-001", "task_status": "verified"}, ...]
15
- """
16
- rows: List[Dict[str, str]] = []
17
- for req in graph.requirements.values():
18
- linked_tasks = [
19
- t for t in graph.tasks.values() if req.id in t.requirement_ids
20
- ]
21
- if not linked_tasks:
22
- rows.append({
23
- "requirement": req.id,
24
- "title": req.title,
25
- "task": "(none)",
26
- "task_status": "NOT PLANNED",
27
- })
28
- else:
29
- for task in linked_tasks:
30
- rows.append({
31
- "requirement": req.id,
32
- "title": req.title,
33
- "task": task.id,
34
- "task_status": task.status,
35
- })
36
- return rows
37
-
38
-
39
- def acceptance_criteria_evidence_matrix(
40
- graph: ArtifactGraph,
41
- ) -> List[Dict[str, str]]:
42
- """Build an AC → evidence mapping for the final report.
43
-
44
- Returns rows with requirement_id, ac_id, ac_description, evidence_status.
45
- """
46
- evidenced: Dict[str, str] = {}
47
- for ev in graph.test_evidence:
48
- key = ev.acceptance_criterion_id
49
- evidenced[key] = ev.status # "passed", "failed", "not_run"
50
-
51
- rows: List[Dict[str, str]] = []
52
- for req in graph.requirements.values():
53
- for ac in req.acceptance_criteria:
54
- status = evidenced.get(ac.id, "no_evidence")
55
- rows.append({
56
- "requirement_id": req.id,
57
- "ac_id": ac.id,
58
- "description": ac.description,
59
- "verification_method": ac.verification_method,
60
- "status": status,
61
- })
62
- return rows
63
-
64
-
65
- def can_approve_plan(graph: ArtifactGraph) -> Tuple[bool, List[str]]:
66
- """Check if the plan meets the PLAN_APPROVED gate criteria.
67
-
68
- Returns (passed, list_of_reasons_if_failed).
69
-
70
- Criteria (from §12):
71
- - Every requirement has acceptance criteria.
72
- - Every acceptance criterion has a verification method.
73
- - Every requirement maps to at least one task.
74
- - Every task maps to at least one requirement.
75
- - Every high-impact assumption is confirmed or converted.
76
- - No critical critique finding remains open.
77
- - No blocking question remains unanswered.
78
- """
79
- reasons: List[str] = []
80
-
81
- for req in graph.requirements_without_acceptance_criteria():
82
- reasons.append(f"{req.id} has no acceptance criteria.")
83
-
84
- for req in graph.requirements_without_tasks():
85
- reasons.append(f"{req.id} is not mapped to any task.")
86
-
87
- for task in graph.tasks_without_requirements():
88
- reasons.append(f"{task.id} is not mapped to any requirement.")
89
-
90
- for asm in graph.unconfirmed_high_impact_assumptions():
91
- reasons.append(f"Assumption {asm.id} (high impact) is still open: {asm.statement}")
92
-
93
- for finding in graph.open_findings("critical"):
94
- reasons.append(f"Critical finding {finding.id} is still open: {finding.claim}")
95
-
96
- return len(reasons) == 0, reasons