devcouncil 0.1.0 → 0.2.0

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 (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -1,190 +1,338 @@
1
- from pydantic import BaseModel
2
- from typing import Any, List, Optional
3
- from pathlib import Path
4
-
5
- from devcouncil.domain.requirement import Requirement
6
- from devcouncil.domain.task import Task
7
- from devcouncil.domain.gap import Gap
8
- from devcouncil.domain.assumption import Assumption
9
- from devcouncil.domain.critique import CritiqueFinding
10
- from devcouncil.gating.checks.requirement_coverage import RequirementCoverageCheck
11
- from devcouncil.gating.checks.planned_files_check import PlannedFilesCheck
12
- from devcouncil.gating.checks.clean_git import CleanGitCheck
13
-
14
- class GateResult(BaseModel):
15
- passed: bool
16
- gaps: List[Gap]
17
-
18
- class GatePolicy:
19
- """Central engine for executing project and task level quality gates."""
20
-
21
- def __init__(self):
22
- self.req_coverage = RequirementCoverageCheck()
23
- self.planned_files = PlannedFilesCheck()
24
- self.clean_git = CleanGitCheck()
25
-
26
- def check_plan_approval(
27
- self,
28
- requirements: List[Requirement],
29
- tasks: List[Task],
30
- assumptions: Optional[List[Assumption]] = None,
31
- findings: Optional[List[CritiqueFinding]] = None,
32
- blocking_questions: Optional[List[Any]] = None,
33
- ) -> GateResult:
34
- """Determines if the overall project plan is ready for execution."""
35
- gaps = []
36
- known_req_ids = {req.id for req in requirements}
37
- known_ac_ids = {
38
- ac.id
39
- for req in requirements
40
- for ac in req.acceptance_criteria
41
- }
42
-
43
- # 1. Check requirement coverage
44
- gaps.extend(self.req_coverage.check(requirements, tasks))
45
-
46
- # 2. Check for acceptance criteria presence
47
- for req in requirements:
48
- if not req.acceptance_criteria:
49
- gaps.append(Gap(
50
- id=f"GAP-PLAN-{req.id}-NO-AC",
51
- severity="high",
52
- gap_type="requirement_not_planned",
53
- requirement_id=req.id,
54
- description=f"Requirement {req.id} has no acceptance criteria.",
55
- recommended_fix="Define at least one measurable AC.",
56
- blocking=True
57
- ))
58
-
59
- for ac in req.acceptance_criteria:
60
- if not ac.verification_method:
61
- gaps.append(Gap(
62
- id=f"GAP-PLAN-{ac.id}-NO-VERIFY",
63
- severity="high",
64
- gap_type="acceptance_criteria_unproven",
65
- requirement_id=req.id,
66
- description=f"Acceptance criterion {ac.id} has no verification method.",
67
- recommended_fix="Define a deterministic verification method for this AC.",
68
- blocking=True,
69
- ))
70
-
71
- for task in tasks:
72
- if not task.requirement_ids:
73
- gaps.append(Gap(
74
- id=f"GAP-PLAN-{task.id}-NO-REQ",
75
- severity="high",
76
- gap_type="requirement_not_planned",
77
- task_id=task.id,
78
- description=f"Task {task.id} is not mapped to any requirement.",
79
- recommended_fix="Map each task to at least one requirement.",
80
- blocking=True,
81
- ))
82
-
83
- unknown_req_ids = [req_id for req_id in task.requirement_ids if req_id not in known_req_ids]
84
- if unknown_req_ids:
85
- gaps.append(Gap(
86
- id=f"GAP-PLAN-{task.id}-UNKNOWN-REQ",
87
- severity="high",
88
- gap_type="requirement_not_planned",
89
- task_id=task.id,
90
- description=f"Task {task.id} references unknown requirement(s): {', '.join(unknown_req_ids)}.",
91
- recommended_fix="Remove invalid requirement links or add the missing requirements.",
92
- blocking=True,
93
- ))
94
-
95
- unknown_ac_ids = [ac_id for ac_id in task.acceptance_criterion_ids if ac_id not in known_ac_ids]
96
- if unknown_ac_ids:
97
- gaps.append(Gap(
98
- id=f"GAP-PLAN-{task.id}-UNKNOWN-AC",
99
- severity="high",
100
- gap_type="acceptance_criteria_unproven",
101
- task_id=task.id,
102
- description=f"Task {task.id} references unknown acceptance criteria: {', '.join(unknown_ac_ids)}.",
103
- recommended_fix="Link tasks only to acceptance criteria declared by requirements.",
104
- blocking=True,
105
- ))
106
-
107
- for assumption in assumptions or []:
108
- if (
109
- assumption.impact == "high"
110
- and assumption.status == "open"
111
- and assumption.requires_user_confirmation
112
- ):
113
- gaps.append(Gap(
114
- id=f"GAP-PLAN-{assumption.id}-OPEN",
115
- severity="high",
116
- gap_type="assumption_violated",
117
- requirement_id=assumption.linked_requirement_ids[0] if assumption.linked_requirement_ids else None,
118
- description=f"High-impact assumption {assumption.id} is still open: {assumption.statement}",
119
- recommended_fix="Confirm, reject, or convert this assumption before approving the plan.",
120
- blocking=True,
121
- ))
122
-
123
- for finding in findings or []:
124
- if finding.severity in {"high", "critical"} and finding.status == "open":
125
- gaps.append(Gap(
126
- id=f"GAP-PLAN-{finding.id}-OPEN",
127
- severity=finding.severity,
128
- gap_type="architecture_drift",
129
- requirement_id=finding.linked_requirement_id,
130
- description=f"Open {finding.severity} critique finding remains: {finding.claim}",
131
- recommended_fix="Convert, rebut with evidence, or mark this finding resolved before approval.",
132
- blocking=True,
133
- ))
134
-
135
- for question in blocking_questions or []:
136
- question_id = getattr(question, "id", "QUESTION")
137
- question_text = getattr(question, "question", str(question))
138
- gaps.append(Gap(
139
- id=f"GAP-PLAN-{question_id}-BLOCKING",
140
- severity="high",
141
- gap_type="requirement_not_planned",
142
- description=f"Blocking question remains unanswered: {question_text}",
143
- recommended_fix="Answer or convert the blocking question before approving the plan.",
144
- blocking=True,
145
- ))
146
-
147
- return GateResult(
148
- passed=len([g for g in gaps if g.blocking]) == 0,
149
- gaps=gaps
150
- )
151
-
1
+ from pydantic import BaseModel
2
+ from typing import Any, List, Optional
3
+ from pathlib import Path
4
+
5
+ from devcouncil.domain.requirement import Requirement
6
+ from devcouncil.domain.task import Task
7
+ from devcouncil.domain.gap import Gap
8
+ from devcouncil.domain.assumption import Assumption
9
+ from devcouncil.domain.critique import CritiqueFinding
10
+ from devcouncil.gating.checks.requirement_coverage import RequirementCoverageCheck
11
+ from devcouncil.gating.checks.planned_files_check import PlannedFilesCheck
12
+ from devcouncil.gating.checks.clean_git import CleanGitCheck
13
+
14
+ class GateResult(BaseModel):
15
+ passed: bool
16
+ gaps: List[Gap]
17
+
18
+
19
+ def _find_dependency_cycle(tasks: List[Task]) -> Optional[List[str]]:
20
+ """Return one dependency cycle as an id path (e.g. [A, B, A]), or None. Only edges
21
+ to known task ids are followed; unknown deps are reported separately."""
22
+ ids = {t.id for t in tasks}
23
+ graph = {t.id: [d for d in t.depends_on if d in ids] for t in tasks}
24
+ WHITE, GREY, BLACK = 0, 1, 2
25
+ color = {tid: WHITE for tid in graph}
26
+ stack: List[str] = []
27
+
28
+ def visit(node: str) -> Optional[List[str]]:
29
+ color[node] = GREY
30
+ stack.append(node)
31
+ for nxt in graph.get(node, []):
32
+ if color[nxt] == GREY:
33
+ return stack[stack.index(nxt):] + [nxt]
34
+ if color[nxt] == WHITE:
35
+ found = visit(nxt)
36
+ if found:
37
+ return found
38
+ stack.pop()
39
+ color[node] = BLACK
40
+ return None
41
+
42
+ for tid in graph:
43
+ if color[tid] == WHITE:
44
+ found = visit(tid)
45
+ if found:
46
+ return found
47
+ return None
48
+
49
+
50
+ def topological_order(tasks: List[Task]) -> List[Task]:
51
+ """Order tasks so every task follows the ones it depends on. Stable: preserves the
52
+ given order among independent tasks. Falls back to the original order if a cycle
53
+ makes a full ordering impossible (the plan gate blocks cycles separately)."""
54
+ by_id = {t.id: t for t in tasks}
55
+ indegree = {t.id: 0 for t in tasks}
56
+ dependents: dict[str, List[str]] = {t.id: [] for t in tasks}
57
+ for task in tasks:
58
+ for dep in task.depends_on:
59
+ if dep in by_id:
60
+ indegree[task.id] += 1
61
+ dependents[dep].append(task.id)
62
+ # Kahn's algorithm, seeded in original order for stability.
63
+ ready = [t.id for t in tasks if indegree[t.id] == 0]
64
+ ordered: List[str] = []
65
+ while ready:
66
+ current = ready.pop(0)
67
+ ordered.append(current)
68
+ for child in dependents[current]:
69
+ indegree[child] -= 1
70
+ if indegree[child] == 0:
71
+ ready.append(child)
72
+ if len(ordered) != len(tasks): # cycle — fall back to original order
73
+ return list(tasks)
74
+ return [by_id[tid] for tid in ordered]
75
+
76
+ class GatePolicy:
77
+ """Central engine for executing project and task level quality gates."""
78
+
79
+ def __init__(self):
80
+ self.req_coverage = RequirementCoverageCheck()
81
+ self.planned_files = PlannedFilesCheck()
82
+ self.clean_git = CleanGitCheck()
83
+
84
+ def check_plan_approval(
85
+ self,
86
+ requirements: List[Requirement],
87
+ tasks: List[Task],
88
+ assumptions: Optional[List[Assumption]] = None,
89
+ findings: Optional[List[CritiqueFinding]] = None,
90
+ blocking_questions: Optional[List[Any]] = None,
91
+ ) -> GateResult:
92
+ """Determines if the overall project plan is ready for execution."""
93
+ gaps = []
94
+ known_req_ids = {req.id for req in requirements}
95
+ known_ac_ids = {
96
+ ac.id
97
+ for req in requirements
98
+ for ac in req.acceptance_criteria
99
+ }
100
+
101
+ # 1. Check requirement coverage
102
+ gaps.extend(self.req_coverage.check(requirements, tasks))
103
+
104
+ # 2. Check for acceptance criteria presence
105
+ for req in requirements:
106
+ if not req.acceptance_criteria:
107
+ gaps.append(Gap(
108
+ id=f"GAP-PLAN-{req.id}-NO-AC",
109
+ severity="high",
110
+ gap_type="requirement_not_planned",
111
+ requirement_id=req.id,
112
+ description=f"Requirement {req.id} has no acceptance criteria.",
113
+ recommended_fix="Define at least one measurable AC.",
114
+ blocking=True
115
+ ))
116
+
117
+ for ac in req.acceptance_criteria:
118
+ if not ac.verification_method:
119
+ gaps.append(Gap(
120
+ id=f"GAP-PLAN-{ac.id}-NO-VERIFY",
121
+ severity="high",
122
+ gap_type="acceptance_criteria_unproven",
123
+ requirement_id=req.id,
124
+ description=f"Acceptance criterion {ac.id} has no verification method.",
125
+ recommended_fix="Define a deterministic verification method for this AC.",
126
+ blocking=True,
127
+ ))
128
+
129
+ for task in tasks:
130
+ if not task.requirement_ids:
131
+ gaps.append(Gap(
132
+ id=f"GAP-PLAN-{task.id}-NO-REQ",
133
+ severity="high",
134
+ gap_type="requirement_not_planned",
135
+ task_id=task.id,
136
+ description=f"Task {task.id} is not mapped to any requirement.",
137
+ recommended_fix="Map each task to at least one requirement.",
138
+ blocking=True,
139
+ ))
140
+
141
+ unknown_req_ids = [req_id for req_id in task.requirement_ids if req_id not in known_req_ids]
142
+ if unknown_req_ids:
143
+ gaps.append(Gap(
144
+ id=f"GAP-PLAN-{task.id}-UNKNOWN-REQ",
145
+ severity="high",
146
+ gap_type="requirement_not_planned",
147
+ task_id=task.id,
148
+ description=f"Task {task.id} references unknown requirement(s): {', '.join(unknown_req_ids)}.",
149
+ recommended_fix="Remove invalid requirement links or add the missing requirements.",
150
+ blocking=True,
151
+ ))
152
+
153
+ unknown_ac_ids = [ac_id for ac_id in task.acceptance_criterion_ids if ac_id not in known_ac_ids]
154
+ if unknown_ac_ids:
155
+ gaps.append(Gap(
156
+ id=f"GAP-PLAN-{task.id}-UNKNOWN-AC",
157
+ severity="high",
158
+ gap_type="acceptance_criteria_unproven",
159
+ task_id=task.id,
160
+ description=f"Task {task.id} references unknown acceptance criteria: {', '.join(unknown_ac_ids)}.",
161
+ recommended_fix="Link tasks only to acceptance criteria declared by requirements.",
162
+ blocking=True,
163
+ ))
164
+
165
+ # Surface read-only-only tasks at PLANNING time (advisory): a task that declares
166
+ # planned files but none writable can implement nothing, and was previously only
167
+ # caught at execution by the task-readiness gate.
168
+ for task in tasks:
169
+ if task.planned_files and not any(
170
+ pf.allowed_change in ("create", "modify", "delete") for pf in task.planned_files
171
+ ):
172
+ gaps.append(Gap(
173
+ id=f"GAP-PLAN-{task.id}-READ-ONLY",
174
+ severity="medium",
175
+ gap_type="task_not_implemented",
176
+ task_id=task.id,
177
+ description=(
178
+ f"Task {task.id} declares planned files but none are writable "
179
+ "(all read_only); it cannot implement any change. Expected only if "
180
+ "this is an analysis-only task."
181
+ ),
182
+ recommended_fix=(
183
+ "Grant 'create', 'modify', or 'delete' to at least one planned file, "
184
+ "or confirm the task is intentionally analysis-only."
185
+ ),
186
+ blocking=False,
187
+ ))
188
+
189
+ # Surface overlapping ownership (advisory): when 2+ tasks each declare a
190
+ # writable (create/modify/delete) change to the SAME file, the plan is
191
+ # over-decomposed — the later task tends to duplicate or conflict with the
192
+ # earlier one (e.g. both add the same function), which then fails per-task
193
+ # verification. Consolidating a file's work into one task avoids this.
194
+ writers_by_file: dict[str, list[str]] = {}
195
+ for task in tasks:
196
+ for pf in task.planned_files:
197
+ if pf.allowed_change in ("create", "modify", "delete"):
198
+ path = pf.path.replace("\\", "/")
199
+ writers_by_file.setdefault(path, [])
200
+ if task.id not in writers_by_file[path]:
201
+ writers_by_file[path].append(task.id)
202
+ for path, owners in writers_by_file.items():
203
+ if len(owners) > 1:
204
+ gaps.append(Gap(
205
+ id=f"GAP-PLAN-OVERLAP-{owners[0]}-{path.replace('/', '_')}",
206
+ severity="medium",
207
+ gap_type="task_not_implemented",
208
+ description=(
209
+ f"{len(owners)} tasks ({', '.join(owners)}) each declare writable "
210
+ f"changes to {path}; overlapping ownership over-decomposes the plan "
211
+ "and tends to cause duplicate/conflicting edits at execution."
212
+ ),
213
+ recommended_fix=(
214
+ f"Consolidate the work on {path} into a single task, or scope the "
215
+ "others to read_only."
216
+ ),
217
+ blocking=False,
218
+ ))
219
+
220
+ # Validate the task dependency DAG: unknown depends_on ids and cycles would make
221
+ # execution ordering impossible / stall the run, so block the plan on them.
222
+ gaps.extend(self._validate_task_dependencies(tasks))
223
+
224
+ for assumption in assumptions or []:
225
+ if (
226
+ assumption.impact == "high"
227
+ and assumption.status == "open"
228
+ and assumption.requires_user_confirmation
229
+ ):
230
+ gaps.append(Gap(
231
+ id=f"GAP-PLAN-{assumption.id}-OPEN",
232
+ severity="high",
233
+ gap_type="assumption_violated",
234
+ requirement_id=assumption.linked_requirement_ids[0] if assumption.linked_requirement_ids else None,
235
+ description=f"High-impact assumption {assumption.id} is still open: {assumption.statement}",
236
+ recommended_fix="Confirm, reject, or convert this assumption before approving the plan.",
237
+ blocking=True,
238
+ ))
239
+
240
+ for finding in findings or []:
241
+ if finding.severity in {"high", "critical"} and finding.status == "open":
242
+ gaps.append(Gap(
243
+ id=f"GAP-PLAN-{finding.id}-OPEN",
244
+ severity=finding.severity,
245
+ gap_type="architecture_drift",
246
+ requirement_id=finding.linked_requirement_id,
247
+ description=f"Open {finding.severity} critique finding remains: {finding.claim}",
248
+ recommended_fix="Convert, rebut with evidence, or mark this finding resolved before approval.",
249
+ blocking=True,
250
+ ))
251
+
252
+ for question in blocking_questions or []:
253
+ question_id = getattr(question, "id", "QUESTION")
254
+ question_text = getattr(question, "question", str(question))
255
+ gaps.append(Gap(
256
+ id=f"GAP-PLAN-{question_id}-BLOCKING",
257
+ severity="high",
258
+ gap_type="requirement_not_planned",
259
+ description=f"Blocking question remains unanswered: {question_text}",
260
+ recommended_fix="Answer or convert the blocking question before approving the plan.",
261
+ blocking=True,
262
+ ))
263
+
264
+ return GateResult(
265
+ passed=len([g for g in gaps if g.blocking]) == 0,
266
+ gaps=gaps
267
+ )
268
+
152
269
  def check_task_ready(self, task: Task, project_root: Path) -> GateResult:
153
270
  """Determines if a specific task can begin execution."""
154
271
  gaps = []
155
-
156
- # 1. Check Git state
157
- gaps.extend(self.clean_git.check(project_root, task.id))
158
-
272
+
273
+ # 1. Check Git state
274
+ gaps.extend(self.clean_git.check(project_root, task.id))
275
+
159
276
  # 2. Check planned files
160
277
  gaps.extend(self.planned_files.check(task))
161
278
 
162
- # 3. Check execution/verification evidence contract
163
- if not task.allowed_commands:
279
+ # 3. Surface a missing execution/verification contract — but do NOT block
280
+ # execution on it. The executor still needs to run to implement the code,
281
+ # and the evidence requirement is genuinely enforced at verify time
282
+ # (acceptance_criteria_unproven / NOAC gaps). Blocking here only prevents
283
+ # implementation and stalls multi-task plans when the planner under-specs a
284
+ # task; these stay advisory so the work can proceed and be judged on output.
285
+ if not task.allowed_commands and not task.expected_tests:
164
286
  gaps.append(Gap(
165
287
  id=f"GAP-{task.id}-NO-COMMANDS",
166
- severity="high",
288
+ severity="medium",
167
289
  gap_type="missing_test",
168
290
  task_id=task.id,
169
291
  description=f"Task {task.id} has no allowed commands for execution or verification.",
170
- recommended_fix="Add explicit allowed_commands for the task before execution.",
171
- blocking=True,
292
+ recommended_fix="Add explicit allowed_commands or expected_tests so verification can prove the acceptance criteria.",
293
+ blocking=False,
172
294
  ))
173
295
 
174
296
  if not task.expected_tests:
175
297
  gaps.append(Gap(
176
298
  id=f"GAP-{task.id}-NO-EXPECTED-EVIDENCE",
177
- severity="high",
299
+ severity="medium",
178
300
  gap_type="missing_test",
179
301
  task_id=task.id,
180
302
  description=f"Task {task.id} has no expected verification evidence.",
181
303
  recommended_fix="Add expected_tests or targeted static/manual review commands that prove the acceptance criteria.",
182
- blocking=True,
304
+ blocking=False,
183
305
  ))
184
306
 
185
- # 4. Check for task dependencies (if implemented)
186
-
187
307
  return GateResult(
188
- passed=len([g for g in gaps if g.blocking]) == 0,
189
- gaps=gaps
190
- )
308
+ passed=len([g for g in gaps if g.blocking]) == 0,
309
+ gaps=gaps
310
+ )
311
+
312
+ def _validate_task_dependencies(self, tasks: List[Task]) -> List[Gap]:
313
+ """Block on a malformed dependency DAG: unknown depends_on ids and cycles."""
314
+ gaps: List[Gap] = []
315
+ ids = {t.id for t in tasks}
316
+ for task in tasks:
317
+ unknown = [dep for dep in task.depends_on if dep not in ids]
318
+ if unknown:
319
+ gaps.append(Gap(
320
+ id=f"GAP-PLAN-{task.id}-UNKNOWN-DEP",
321
+ severity="high",
322
+ gap_type="task_not_implemented",
323
+ task_id=task.id,
324
+ description=f"Task {task.id} depends on unknown task(s): {', '.join(unknown)}.",
325
+ recommended_fix="Reference only task IDs that exist in this plan, or remove the dependency.",
326
+ blocking=True,
327
+ ))
328
+ cycle = _find_dependency_cycle(tasks)
329
+ if cycle:
330
+ gaps.append(Gap(
331
+ id="GAP-PLAN-DEP-CYCLE",
332
+ severity="high",
333
+ gap_type="task_not_implemented",
334
+ description=f"Task dependency cycle detected: {' -> '.join(cycle)}.",
335
+ recommended_fix="Break the cycle so the tasks can be ordered and executed.",
336
+ blocking=True,
337
+ ))
338
+ return gaps