devcouncil 0.2.0 → 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 (205) hide show
  1. package/README.md +191 -253
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +6 -17
  4. package/pyproject.toml +0 -66
  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 -320
  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 -163
  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/assets/__init__.py +0 -1
  23. package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
  24. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  25. package/src/devcouncil/cli/__init__.py +0 -0
  26. package/src/devcouncil/cli/commands/__init__.py +0 -0
  27. package/src/devcouncil/cli/commands/agents.py +0 -292
  28. package/src/devcouncil/cli/commands/artifacts.py +0 -54
  29. package/src/devcouncil/cli/commands/ast.py +0 -22
  30. package/src/devcouncil/cli/commands/baseline.py +0 -35
  31. package/src/devcouncil/cli/commands/check.py +0 -209
  32. package/src/devcouncil/cli/commands/config.py +0 -115
  33. package/src/devcouncil/cli/commands/cost.py +0 -57
  34. package/src/devcouncil/cli/commands/dashboard.py +0 -31
  35. package/src/devcouncil/cli/commands/doctor.py +0 -340
  36. package/src/devcouncil/cli/commands/evidence.py +0 -48
  37. package/src/devcouncil/cli/commands/go.py +0 -656
  38. package/src/devcouncil/cli/commands/handoff.py +0 -69
  39. package/src/devcouncil/cli/commands/hook.py +0 -237
  40. package/src/devcouncil/cli/commands/init.py +0 -289
  41. package/src/devcouncil/cli/commands/integrate.py +0 -1515
  42. package/src/devcouncil/cli/commands/lsp.py +0 -20
  43. package/src/devcouncil/cli/commands/map.py +0 -112
  44. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  45. package/src/devcouncil/cli/commands/plan.py +0 -488
  46. package/src/devcouncil/cli/commands/prompt.py +0 -61
  47. package/src/devcouncil/cli/commands/repair.py +0 -89
  48. package/src/devcouncil/cli/commands/report.py +0 -137
  49. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
  50. package/src/devcouncil/cli/commands/rollback.py +0 -59
  51. package/src/devcouncil/cli/commands/run.py +0 -289
  52. package/src/devcouncil/cli/commands/runs.py +0 -223
  53. package/src/devcouncil/cli/commands/scaffold.py +0 -32
  54. package/src/devcouncil/cli/commands/semantic.py +0 -47
  55. package/src/devcouncil/cli/commands/setup.py +0 -362
  56. package/src/devcouncil/cli/commands/shell.py +0 -73
  57. package/src/devcouncil/cli/commands/show.py +0 -76
  58. package/src/devcouncil/cli/commands/skills.py +0 -88
  59. package/src/devcouncil/cli/commands/status.py +0 -141
  60. package/src/devcouncil/cli/commands/tasks.py +0 -55
  61. package/src/devcouncil/cli/commands/trace.py +0 -88
  62. package/src/devcouncil/cli/commands/verify.py +0 -328
  63. package/src/devcouncil/cli/commands/version.py +0 -20
  64. package/src/devcouncil/cli/commands/watch.py +0 -574
  65. package/src/devcouncil/cli/commands/watch_fs.py +0 -40
  66. package/src/devcouncil/cli/main.py +0 -137
  67. package/src/devcouncil/council/__init__.py +0 -0
  68. package/src/devcouncil/council/prompts/__init__.py +0 -0
  69. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  70. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  71. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  72. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  73. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  74. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  75. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  76. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  77. package/src/devcouncil/domain/__init__.py +0 -0
  78. package/src/devcouncil/domain/assumption.py +0 -17
  79. package/src/devcouncil/domain/critique.py +0 -32
  80. package/src/devcouncil/domain/evidence.py +0 -47
  81. package/src/devcouncil/domain/gap.py +0 -52
  82. package/src/devcouncil/domain/requirement.py +0 -22
  83. package/src/devcouncil/domain/task.py +0 -55
  84. package/src/devcouncil/execution/__init__.py +0 -1
  85. package/src/devcouncil/execution/checkpoints.py +0 -246
  86. package/src/devcouncil/execution/context_builder.py +0 -60
  87. package/src/devcouncil/execution/executor.py +0 -15
  88. package/src/devcouncil/execution/fs_watcher.py +0 -180
  89. package/src/devcouncil/execution/handoff.py +0 -102
  90. package/src/devcouncil/execution/hook_policy.py +0 -253
  91. package/src/devcouncil/execution/patch.py +0 -77
  92. package/src/devcouncil/execution/paths.py +0 -14
  93. package/src/devcouncil/execution/permissions.py +0 -85
  94. package/src/devcouncil/execution/policy_engine.py +0 -343
  95. package/src/devcouncil/execution/prompt_builder.py +0 -671
  96. package/src/devcouncil/execution/shell_session.py +0 -225
  97. package/src/devcouncil/execution/task_runner.py +0 -170
  98. package/src/devcouncil/executors/__init__.py +0 -1
  99. package/src/devcouncil/executors/agent_registry.py +0 -575
  100. package/src/devcouncil/executors/coding_cli.py +0 -736
  101. package/src/devcouncil/executors/mini_swe.py +0 -73
  102. package/src/devcouncil/executors/native/__init__.py +0 -0
  103. package/src/devcouncil/executors/native/agent.py +0 -208
  104. package/src/devcouncil/executors/openhands.py +0 -71
  105. package/src/devcouncil/gating/__init__.py +0 -1
  106. package/src/devcouncil/gating/checks/__init__.py +0 -0
  107. package/src/devcouncil/gating/checks/clean_git.py +0 -52
  108. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  109. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  110. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
  111. package/src/devcouncil/gating/policy.py +0 -338
  112. package/src/devcouncil/hardware.py +0 -184
  113. package/src/devcouncil/indexing/__init__.py +0 -1
  114. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  115. package/src/devcouncil/indexing/graph_index.py +0 -48
  116. package/src/devcouncil/indexing/lsp.py +0 -161
  117. package/src/devcouncil/indexing/repo_mapper.py +0 -1455
  118. package/src/devcouncil/indexing/semantic_index.py +0 -205
  119. package/src/devcouncil/integrations/actions.py +0 -146
  120. package/src/devcouncil/integrations/check.py +0 -423
  121. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  122. package/src/devcouncil/integrations/github.py +0 -39
  123. package/src/devcouncil/integrations/github_intent.py +0 -142
  124. package/src/devcouncil/integrations/gitnexus.py +0 -62
  125. package/src/devcouncil/integrations/graphify.py +0 -34
  126. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  127. package/src/devcouncil/integrations/mcp/server.py +0 -2122
  128. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
  129. package/src/devcouncil/integrations/pr_comments.py +0 -62
  130. package/src/devcouncil/live/__init__.py +0 -2
  131. package/src/devcouncil/live/cards.py +0 -349
  132. package/src/devcouncil/live/models.py +0 -63
  133. package/src/devcouncil/live/repair_prompt.py +0 -83
  134. package/src/devcouncil/live/reviewer.py +0 -70
  135. package/src/devcouncil/live/signals.py +0 -135
  136. package/src/devcouncil/live/summary.py +0 -34
  137. package/src/devcouncil/live/tasks.py +0 -18
  138. package/src/devcouncil/live/transcripts.py +0 -141
  139. package/src/devcouncil/llm/__init__.py +0 -1
  140. package/src/devcouncil/llm/cache.py +0 -42
  141. package/src/devcouncil/llm/model_defaults.yaml +0 -44
  142. package/src/devcouncil/llm/provider.py +0 -627
  143. package/src/devcouncil/llm/router.py +0 -310
  144. package/src/devcouncil/optimization/__init__.py +0 -1
  145. package/src/devcouncil/optimization/gepa_agent.py +0 -318
  146. package/src/devcouncil/planning/__init__.py +0 -1
  147. package/src/devcouncil/planning/arbiter_service.py +0 -57
  148. package/src/devcouncil/planning/correction_manifest.py +0 -303
  149. package/src/devcouncil/planning/critique_service.py +0 -71
  150. package/src/devcouncil/planning/plan_service.py +0 -60
  151. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
  152. package/src/devcouncil/planning/repair_service.py +0 -39
  153. package/src/devcouncil/planning/spec_service.py +0 -70
  154. package/src/devcouncil/repo/__init__.py +0 -0
  155. package/src/devcouncil/repo/ci_scaffold.py +0 -157
  156. package/src/devcouncil/repo/gitignore.py +0 -123
  157. package/src/devcouncil/repo/sca.py +0 -374
  158. package/src/devcouncil/reporting/__init__.py +0 -0
  159. package/src/devcouncil/reporting/github_check.py +0 -32
  160. package/src/devcouncil/reporting/json_report.py +0 -30
  161. package/src/devcouncil/reporting/markdown_report.py +0 -83
  162. package/src/devcouncil/reporting/report_builder.py +0 -14
  163. package/src/devcouncil/skills/__init__.py +0 -19
  164. package/src/devcouncil/skills/library/README.md +0 -46
  165. package/src/devcouncil/skills/library/ai-training.md +0 -50
  166. package/src/devcouncil/skills/library/android.md +0 -50
  167. package/src/devcouncil/skills/library/backend.md +0 -52
  168. package/src/devcouncil/skills/library/core-engineering.md +0 -95
  169. package/src/devcouncil/skills/library/data-engineering.md +0 -47
  170. package/src/devcouncil/skills/library/desktop.md +0 -46
  171. package/src/devcouncil/skills/library/devops.md +0 -48
  172. package/src/devcouncil/skills/library/game-dev.md +0 -46
  173. package/src/devcouncil/skills/library/ios.md +0 -48
  174. package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
  175. package/src/devcouncil/skills/library/security.md +0 -48
  176. package/src/devcouncil/skills/library/systems.md +0 -48
  177. package/src/devcouncil/skills/library/web.md +0 -47
  178. package/src/devcouncil/skills/library/windows.md +0 -47
  179. package/src/devcouncil/skills/registry.py +0 -330
  180. package/src/devcouncil/storage/__init__.py +0 -0
  181. package/src/devcouncil/storage/db.py +0 -147
  182. package/src/devcouncil/storage/models.py +0 -204
  183. package/src/devcouncil/storage/native.py +0 -557
  184. package/src/devcouncil/storage/repositories.py +0 -485
  185. package/src/devcouncil/telemetry/__init__.py +0 -0
  186. package/src/devcouncil/telemetry/cost.py +0 -140
  187. package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
  188. package/src/devcouncil/telemetry/pricing.py +0 -28
  189. package/src/devcouncil/telemetry/traces.py +0 -146
  190. package/src/devcouncil/telemetry/tracker.py +0 -52
  191. package/src/devcouncil/ui/__init__.py +0 -1
  192. package/src/devcouncil/ui/dashboard.py +0 -423
  193. package/src/devcouncil/utils/__init__.py +0 -1
  194. package/src/devcouncil/utils/redaction.py +0 -147
  195. package/src/devcouncil/utils/subprocess_env.py +0 -69
  196. package/src/devcouncil/verification/__init__.py +0 -1
  197. package/src/devcouncil/verification/acceptance_compiler.py +0 -125
  198. package/src/devcouncil/verification/ad_hoc_check.py +0 -129
  199. package/src/devcouncil/verification/diff_coverage.py +0 -353
  200. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  201. package/src/devcouncil/verification/next_actions.py +0 -189
  202. package/src/devcouncil/verification/sandbox.py +0 -178
  203. package/src/devcouncil/verification/test_resolver.py +0 -91
  204. package/src/devcouncil/verification/verifier.py +0 -1548
  205. package/uv.lock +0 -1226
@@ -1,343 +0,0 @@
1
- """Shared task policy engine for shell commands and file changes."""
2
-
3
- from __future__ import annotations
4
-
5
- import fnmatch
6
- import re
7
- from pathlib import Path
8
- from typing import Literal
9
-
10
- from pydantic import BaseModel
11
-
12
- from devcouncil.domain.task import PlannedFile, Task
13
-
14
-
15
- class PolicyDecision(BaseModel):
16
- action: Literal["allow", "warn", "deny"]
17
- reason: str
18
- target: str
19
- task_id: str | None = None
20
-
21
-
22
- def normalize_repo_path(project_root: Path, raw_path: str) -> tuple[str, bool]:
23
- """Resolve ``raw_path`` against ``project_root`` and report containment.
24
-
25
- Returns ``(normalized_posix_path, is_outside_root)``. This is the single source of
26
- truth used by both the task policy engine and the coding-CLI hook policy, so a path
27
- is normalized identically wherever it is checked — closing the bypass where a path
28
- that failed to resolve was returned raw and enforced differently than it was
29
- checked. ``is_outside_root`` is True for anything that escapes the project (absolute
30
- elsewhere, ``..`` traversal) or cannot be resolved, so callers fail closed."""
31
- cleaned = raw_path.strip().strip('"').replace("\\", "/")
32
- root = project_root.resolve()
33
- try:
34
- candidate = Path(cleaned)
35
- resolved = candidate.resolve() if candidate.is_absolute() else (root / cleaned).resolve()
36
- except OSError:
37
- fallback = cleaned[2:] if cleaned.startswith("./") else cleaned
38
- return fallback, True
39
- try:
40
- return resolved.relative_to(root).as_posix(), False
41
- except ValueError:
42
- return resolved.as_posix(), True
43
-
44
-
45
- _NO_TASK_ALLOWED_COMMANDS = (
46
- "dev status",
47
- "dev tasks",
48
- "git status",
49
- "git diff",
50
- "git diff *",
51
- )
52
-
53
- # Shared with HookPolicy — keep these as the single source of truth for
54
- # protected/secret path patterns.
55
- PROTECTED_WRITE_PATTERNS = (
56
- "package.json",
57
- "pyproject.toml",
58
- "uv.lock",
59
- "package-lock.json",
60
- "yarn.lock",
61
- "pnpm-lock.yaml",
62
- "Dockerfile",
63
- "docker-compose.yml",
64
- ".github/workflows/*.yml",
65
- ".github/workflows/*.yaml",
66
- "schema.prisma",
67
- "wrangler.toml",
68
- "index.html",
69
- )
70
-
71
- SECRET_PATH_PATTERNS = (
72
- ".env",
73
- ".env.*",
74
- "**/.env",
75
- "**/.env.*",
76
- "**/credentials/**",
77
- "**/secrets/**",
78
- "**/*.pem",
79
- "**/*.key",
80
- # Private SSH keys and well-known credential/token files. These are never
81
- # writable through the gate so an agent cannot plant or overwrite credentials.
82
- "**/id_rsa",
83
- "**/id_dsa",
84
- "**/id_ecdsa",
85
- "**/id_ed25519",
86
- "id_rsa",
87
- "id_ed25519",
88
- ".npmrc",
89
- "**/.npmrc",
90
- ".pypirc",
91
- "**/.pypirc",
92
- ".netrc",
93
- "**/.netrc",
94
- "**/.aws/credentials",
95
- ".aws/credentials",
96
- "**/*.pfx",
97
- "**/*.p12",
98
- "*.pfx",
99
- "*.p12",
100
- ".git-credentials",
101
- "**/.git-credentials",
102
- "**/kube/config",
103
- "**/.kube/config",
104
- )
105
-
106
- _RESTRICTED_PATH_PATTERNS = (
107
- ".git/*",
108
- ".devcouncil/*",
109
- # Protect the client hook/agent configs themselves: an agent must not be able to
110
- # disarm or rewire the pre-action gate by editing its own client integration files.
111
- ".claude/*",
112
- ".claude/**",
113
- ".codex/*",
114
- ".codex/**",
115
- ".cursor/*",
116
- ".cursor/**",
117
- ".gemini/*",
118
- ".gemini/**",
119
- ".opencode/*",
120
- ".opencode/**",
121
- ".agents/*",
122
- ".agents/**",
123
- "opencode.json",
124
- )
125
-
126
-
127
- class TaskPolicyEngine:
128
- def __init__(
129
- self,
130
- project_root: Path,
131
- global_allowed_commands: list[str] | None = None,
132
- ):
133
- self.project_root = project_root.resolve()
134
- self.global_allowed_commands = global_allowed_commands or []
135
-
136
- def evaluate_command(self, command: str, task: Task | None) -> PolicyDecision:
137
- normalized = " ".join(command.split())
138
- if not normalized:
139
- return PolicyDecision(
140
- action="deny",
141
- reason="Empty command is not allowed.",
142
- target=command,
143
- task_id=task.id if task else None,
144
- )
145
-
146
- if task is None:
147
- if any(fnmatch.fnmatch(normalized, allowed) for allowed in _NO_TASK_ALLOWED_COMMANDS):
148
- return PolicyDecision(
149
- action="allow",
150
- reason="Read-only command allowed without active task.",
151
- target=normalized,
152
- )
153
- return PolicyDecision(
154
- action="deny",
155
- reason="Shell commands require an active task lease.",
156
- target=normalized,
157
- )
158
-
159
- if any(fnmatch.fnmatch(normalized, allowed) for allowed in task.allowed_commands):
160
- return PolicyDecision(
161
- action="allow",
162
- reason="Command matches task allowed_commands.",
163
- target=normalized,
164
- task_id=task.id,
165
- )
166
- if any(fnmatch.fnmatch(normalized, allowed) for allowed in self.global_allowed_commands):
167
- return PolicyDecision(
168
- action="allow",
169
- reason="Command matches global allowed commands.",
170
- target=normalized,
171
- task_id=task.id,
172
- )
173
- return PolicyDecision(
174
- action="deny",
175
- reason="Command is not in task or global allowlists.",
176
- target=normalized,
177
- task_id=task.id,
178
- )
179
-
180
- def evaluate_file_change(
181
- self,
182
- path: str,
183
- task: Task | None,
184
- operation: Literal["create", "modify", "delete", "write"] = "write",
185
- *,
186
- internal: bool = False,
187
- ) -> PolicyDecision:
188
- normalized, outside_root = normalize_repo_path(self.project_root, path)
189
- task_id = task.id if task else None
190
-
191
- if outside_root:
192
- return PolicyDecision(
193
- action="deny",
194
- reason="Path is outside the project root.",
195
- target=normalized,
196
- task_id=task_id,
197
- )
198
-
199
- if self._matches_any(normalized, SECRET_PATH_PATTERNS):
200
- return PolicyDecision(
201
- action="deny",
202
- reason="Secret and credential paths are never writable.",
203
- target=normalized,
204
- task_id=task_id,
205
- )
206
-
207
- if not internal and self._matches_restricted(normalized):
208
- return PolicyDecision(
209
- action="deny",
210
- reason="Protected repository paths cannot be modified.",
211
- target=normalized,
212
- task_id=task_id,
213
- )
214
-
215
- if task is None:
216
- return PolicyDecision(
217
- action="deny",
218
- reason="No running DevCouncil task authorizes this file write.",
219
- target=normalized,
220
- )
221
-
222
- if self._matches_forbidden(normalized, task):
223
- return PolicyDecision(
224
- action="deny",
225
- reason="Path is listed in forbidden_changes.",
226
- target=normalized,
227
- task_id=task.id,
228
- )
229
-
230
- planned = self._planned_file_for(normalized, task)
231
- if planned is None:
232
- return PolicyDecision(
233
- action="deny",
234
- reason=f"Task {task.id} does not authorize changes to {normalized}.",
235
- target=normalized,
236
- task_id=task.id,
237
- )
238
-
239
- if planned.allowed_change == "read_only":
240
- return PolicyDecision(
241
- action="deny",
242
- reason="Planned file is read-only.",
243
- target=normalized,
244
- task_id=task.id,
245
- )
246
- if operation == "write":
247
- if planned.allowed_change not in {"create", "modify"}:
248
- return PolicyDecision(
249
- action="deny",
250
- reason=f"Operation {operation} not allowed for planned file.",
251
- target=normalized,
252
- task_id=task.id,
253
- )
254
- elif planned.allowed_change != operation:
255
- return PolicyDecision(
256
- action="deny",
257
- reason=f"Operation {operation} not allowed for planned file.",
258
- target=normalized,
259
- task_id=task.id,
260
- )
261
-
262
- if self._matches_any(normalized, PROTECTED_WRITE_PATTERNS):
263
- return PolicyDecision(
264
- action="warn",
265
- reason=f"{normalized} is a protected high-impact file; verification gates must approve it.",
266
- target=normalized,
267
- task_id=task.id,
268
- )
269
-
270
- return PolicyDecision(
271
- action="allow",
272
- reason="File change is allowed.",
273
- target=normalized,
274
- task_id=task.id,
275
- )
276
-
277
- def evaluate_hook_command(self, command: str) -> PolicyDecision:
278
- """Git safety checks for hook/shell tool paths."""
279
- normalized = " ".join(command.split())
280
- lowered = normalized.lower()
281
- if not normalized:
282
- return PolicyDecision(action="allow", reason="No command detected.", target=normalized)
283
-
284
- if "--no-verify" in lowered or "--no-gpg-sign" in lowered:
285
- return PolicyDecision(
286
- action="deny",
287
- reason="Verification bypass flags are not allowed.",
288
- target=normalized,
289
- )
290
-
291
- if re.search(r"\bgit\s+reset\s+--hard\s+(origin/)?(main|master)\b", lowered):
292
- return PolicyDecision(
293
- action="deny",
294
- reason="Protected branch hard resets are not allowed.",
295
- target=normalized,
296
- )
297
-
298
- if re.search(r"\bgit\s+push\b.*(\s--force(?:-with-lease)?\b|\s-f\b)", lowered) or re.search(
299
- r"\bgit\s+push\s+\S+\s+\+\S", lowered
300
- ):
301
- # The second pattern catches the leading-plus refspec form
302
- # (`git push origin +HEAD:master`), which forces a non-fast-forward update
303
- # without the --force flag.
304
- return PolicyDecision(
305
- action="deny",
306
- reason="Force pushes are not allowed.",
307
- target=normalized,
308
- )
309
-
310
- if re.search(r"\bgit\s+push\s+\S+\s+((head:)?(main|master)|(main|master):\S+)\b", lowered):
311
- return PolicyDecision(
312
- action="warn",
313
- reason="Direct pushes to protected branches should go through verification gates.",
314
- target=normalized,
315
- )
316
-
317
- return PolicyDecision(action="allow", reason="Command is allowed.", target=normalized)
318
-
319
- def _normalize_path(self, raw_path: str) -> str:
320
- return normalize_repo_path(self.project_root, raw_path)[0]
321
-
322
- def _planned_file_for(self, path: str, task: Task) -> PlannedFile | None:
323
- for planned in task.planned_files:
324
- planned_path = planned.path.replace("\\", "/")
325
- if path == planned_path or fnmatch.fnmatch(path, planned_path):
326
- return planned
327
- return None
328
-
329
- def _matches_forbidden(self, path: str, task: Task) -> bool:
330
- for forbidden in task.forbidden_changes:
331
- pattern = forbidden.replace("\\", "/")
332
- if path == pattern or fnmatch.fnmatch(path, pattern):
333
- return True
334
- return False
335
-
336
- def _matches_restricted(self, path: str) -> bool:
337
- for pattern in _RESTRICTED_PATH_PATTERNS:
338
- if fnmatch.fnmatch(path, pattern) or path.startswith(pattern.strip("*")):
339
- return True
340
- return False
341
-
342
- def _matches_any(self, path: str, patterns: tuple[str, ...]) -> bool:
343
- return any(fnmatch.fnmatch(path, pattern) for pattern in patterns)