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,205 +0,0 @@
1
- """Semantic snapshots and diff classification."""
2
-
3
- from __future__ import annotations
4
-
5
- import ast
6
- import hashlib
7
- import json
8
- import re
9
- from datetime import datetime, timezone
10
- from pathlib import Path
11
-
12
- from devcouncil.indexing.ast_matcher import AstMatcher
13
- from devcouncil.indexing.lsp import LspInspector
14
- from devcouncil.storage.db import get_db
15
- from devcouncil.storage.native import SemanticDiffRepository
16
-
17
- _CONFIG_FILES = {
18
- "pyproject.toml",
19
- "package.json",
20
- "uv.lock",
21
- "schema.prisma",
22
- "docker-compose.yml",
23
- "Dockerfile",
24
- }
25
-
26
-
27
- class SemanticIndex:
28
- def __init__(self, project_root: Path):
29
- self.project_root = project_root.resolve()
30
- self.semantic_dir = self.project_root / ".devcouncil" / "semantic"
31
- self.matcher = AstMatcher(self.project_root)
32
-
33
- def snapshot_path(self, task_id: str, stage: str) -> Path:
34
- return self.semantic_dir / task_id / f"{stage}.json"
35
-
36
- def create_snapshot(self, task_id: str, stage: str) -> Path:
37
- symbols = self._collect_symbols()
38
- payload = {
39
- "task_id": task_id,
40
- "created_at": datetime.now(timezone.utc).isoformat(),
41
- "repo_map_path": str(self.project_root / ".devcouncil" / "repo_map.json"),
42
- "files": self._config_file_entries(),
43
- "source_files": self._source_file_entries(),
44
- "symbols": symbols,
45
- "imports": self._collect_imports(),
46
- "public_symbols": [s for s in symbols if s.get("public")],
47
- "lsp": json.loads(LspInspector(self.project_root).summary_json()),
48
- }
49
- path = self.snapshot_path(task_id, stage)
50
- path.parent.mkdir(parents=True, exist_ok=True)
51
- path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
52
- return path
53
-
54
- def diff(self, task_id: str) -> dict:
55
- before_path = self.snapshot_path(task_id, "before")
56
- after_path = self.snapshot_path(task_id, "after")
57
- if not after_path.exists():
58
- self.create_snapshot(task_id, "after")
59
- before = json.loads(before_path.read_text(encoding="utf-8")) if before_path.exists() else {}
60
- after = json.loads(after_path.read_text(encoding="utf-8"))
61
- classifications = self._classify(before, after)
62
- summary = ", ".join(item["type"] for item in classifications) or "no semantic changes"
63
- db = get_db(self.project_root)
64
- if db:
65
- with db.get_session() as session:
66
- SemanticDiffRepository(session).save(
67
- task_id,
68
- str(before_path),
69
- str(after_path),
70
- classifications,
71
- summary,
72
- )
73
- return {"classifications": classifications, "summary": summary}
74
-
75
- def _collect_symbols(self) -> list[dict]:
76
- symbols: list[dict] = []
77
- for match in self.matcher.match(limit=500):
78
- symbols.append({
79
- "path": match.path,
80
- "language": match.language,
81
- "kind": match.kind,
82
- "name": match.name,
83
- "line": match.line,
84
- "signature": match.text.strip(),
85
- "public": match.name[:1].isupper() or "export" in match.text,
86
- })
87
- return symbols
88
-
89
- def _collect_imports(self) -> list[dict]:
90
- imports: list[dict] = []
91
- for path in self.project_root.rglob("*"):
92
- if not path.is_file() or path.suffix not in {".py", ".ts", ".tsx", ".js", ".go", ".rs"}:
93
- continue
94
- rel = path.relative_to(self.project_root).as_posix()
95
- # Filter on the path relative to the project root — filtering on the
96
- # absolute path would skip everything when the repo itself lives
97
- # under a dot-directory.
98
- rel_parts = Path(rel).parts
99
- if self._is_ignored_path(rel) or any(part.startswith(".") for part in rel_parts):
100
- continue
101
- source = path.read_text(encoding="utf-8", errors="replace")
102
- if path.suffix == ".py":
103
- try:
104
- tree = ast.parse(source)
105
- except (SyntaxError, ValueError):
106
- continue
107
- for node in ast.walk(tree):
108
- if isinstance(node, (ast.Import, ast.ImportFrom)):
109
- segment = ast.get_source_segment(source, node) or ""
110
- imports.append({"path": rel, "statement": segment})
111
- else:
112
- for line in source.splitlines():
113
- if re.match(r"^\s*(import|from)\s+", line):
114
- imports.append({"path": rel, "statement": line.strip()})
115
- return imports
116
-
117
- def _classify(self, before: dict, after: dict) -> list[dict]:
118
- before_symbols = {(s["path"], s["name"]): s for s in before.get("symbols", [])}
119
- after_symbols = {(s["path"], s["name"]): s for s in after.get("symbols", [])}
120
- results: list[dict] = []
121
-
122
- for key, after_sym in after_symbols.items():
123
- before_sym = before_symbols.get(key)
124
- path = after_sym["path"]
125
- if path.startswith("tests/") or "/test_" in path:
126
- results.append({"type": "test_only_change", "path": path, "name": after_sym["name"]})
127
- continue
128
- if Path(path).name in _CONFIG_FILES:
129
- results.append({"type": "config_schema_dependency_change", "path": path, "name": after_sym["name"]})
130
- continue
131
- if before_sym is None and after_sym.get("public"):
132
- results.append({"type": "exported_symbol_added", "path": path, "name": after_sym["name"]})
133
- elif before_sym and before_sym.get("signature") != after_sym.get("signature"):
134
- if after_sym.get("public"):
135
- results.append({"type": "public_api_change", "path": path, "name": after_sym["name"]})
136
- else:
137
- results.append({"type": "private_implementation_change", "path": path, "name": after_sym["name"]})
138
-
139
- before_imports = {(i["path"], i["statement"]) for i in before.get("imports", [])}
140
- after_imports = {(i["path"], i["statement"]) for i in after.get("imports", [])}
141
- for added in after_imports - before_imports:
142
- results.append({"type": "import_dependency_change", "path": added[0], "statement": added[1]})
143
-
144
- for key in before_symbols:
145
- if key not in after_symbols and before_symbols[key].get("public"):
146
- results.append({
147
- "type": "exported_symbol_removed",
148
- "path": before_symbols[key]["path"],
149
- "name": before_symbols[key]["name"],
150
- })
151
-
152
- before_files = {item["path"]: item.get("content", "") for item in before.get("files", []) if isinstance(item, dict)}
153
- after_files = {item["path"]: item.get("content", "") for item in after.get("files", []) if isinstance(item, dict)}
154
- for path, content in after_files.items():
155
- if Path(path).name in _CONFIG_FILES and before_files.get(path) != content:
156
- results.append({"type": "config_schema_dependency_change", "path": path})
157
-
158
- classified_paths = {item.get("path") for item in results}
159
- before_source = {
160
- item["path"]: item.get("sha256", "")
161
- for item in before.get("source_files", [])
162
- if isinstance(item, dict)
163
- }
164
- after_source = {
165
- item["path"]: item.get("sha256", "")
166
- for item in after.get("source_files", [])
167
- if isinstance(item, dict)
168
- }
169
- for path, digest in after_source.items():
170
- if path in classified_paths:
171
- continue
172
- if before_source.get(path) == digest:
173
- continue
174
- if path.startswith("tests/") or "/test_" in path:
175
- results.append({"type": "test_only_change", "path": path})
176
- elif Path(path).name not in _CONFIG_FILES:
177
- results.append({"type": "private_implementation_change", "path": path})
178
- return results
179
-
180
- def _config_file_entries(self) -> list[dict]:
181
- entries: list[dict] = []
182
- for name in _CONFIG_FILES:
183
- path = self.project_root / name
184
- if path.exists():
185
- entries.append({
186
- "path": name,
187
- "content": path.read_text(encoding="utf-8", errors="replace"),
188
- })
189
- return entries
190
-
191
- def _source_file_entries(self) -> list[dict]:
192
- entries: list[dict] = []
193
- for path in self.project_root.rglob("*"):
194
- if not path.is_file() or path.suffix not in {".py", ".ts", ".tsx", ".js", ".go", ".rs"}:
195
- continue
196
- rel = path.relative_to(self.project_root).as_posix()
197
- if self._is_ignored_path(rel):
198
- continue
199
- raw = path.read_bytes()
200
- entries.append({"path": rel, "sha256": hashlib.sha256(raw).hexdigest()})
201
- return sorted(entries, key=lambda item: item["path"])
202
-
203
- def _is_ignored_path(self, rel_path: str) -> bool:
204
- ignored_parts = {".git", ".devcouncil", "__pycache__", ".venv", "node_modules", "dist", "build", "target", "vendor"}
205
- return any(part in ignored_parts for part in Path(rel_path).parts)
@@ -1,146 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import shutil
5
- from dataclasses import dataclass
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- from devcouncil.integrations.check import build_integration_check_report
10
-
11
- VALID_INTEGRATION_TARGETS = {
12
- "all",
13
- "hooks",
14
- "codex",
15
- "gemini",
16
- "claude",
17
- "cursor",
18
- "opencode",
19
- "antigravity",
20
- "agy",
21
- "warp",
22
- "aider",
23
- }
24
-
25
- TARGET_ALIASES = {
26
- "agy": "antigravity",
27
- }
28
-
29
-
30
- @dataclass(frozen=True)
31
- class IntegrationActionReport:
32
- target: str
33
- ok: bool
34
- results: list[dict[str, Any]]
35
- warnings: list[str]
36
- check: dict[str, Any]
37
-
38
- def as_dict(self) -> dict[str, Any]:
39
- return {
40
- "target": self.target,
41
- "ok": self.ok,
42
- "results": self.results,
43
- "warnings": self.warnings,
44
- "check": self.check,
45
- }
46
-
47
- def to_json(self, *, indent: int | None = 2) -> str:
48
- return json.dumps(self.as_dict(), indent=indent)
49
-
50
-
51
- def normalize_apply_target(target: str) -> str:
52
- normalized = (target or "").strip().lower().replace("_", "-")
53
- normalized = TARGET_ALIASES.get(normalized, normalized)
54
- if normalized not in VALID_INTEGRATION_TARGETS:
55
- allowed = ", ".join(sorted(VALID_INTEGRATION_TARGETS))
56
- raise ValueError(f"Unsupported integration target '{target}'. Expected one of: {allowed}.")
57
- return normalized
58
-
59
-
60
- def apply_integration_target(
61
- project_root: Path,
62
- target: str,
63
- *,
64
- include_hooks: bool = True,
65
- strict: bool = False,
66
- gemini_scope: str = "project",
67
- claude_scope: str = "local",
68
- ) -> IntegrationActionReport:
69
- from devcouncil.cli.commands import integrate
70
-
71
- root = project_root.expanduser().resolve()
72
- normalized = normalize_apply_target(target)
73
- warnings: list[str] = []
74
- results: list[dict[str, Any]] = []
75
-
76
- def add_result(name: str, ok: bool, path: Path | None = None, message: str = "") -> None:
77
- results.append({
78
- "target": name,
79
- "ok": ok,
80
- "path": str(path.relative_to(root)) if path and path.is_relative_to(root) else (str(path) if path else ""),
81
- "message": message,
82
- })
83
-
84
- def apply_first_party(name: str) -> None:
85
- command_builders = {
86
- "codex": integrate._codex_command,
87
- "gemini": lambda project: integrate._gemini_command(project, gemini_scope),
88
- "claude": lambda project: integrate._claude_command(project, claude_scope),
89
- }
90
- command = command_builders[name](root)
91
- if not shutil.which(command[0]):
92
- warning = f"{name} CLI not found on PATH; skipped CLI MCP registration."
93
- warnings.append(warning)
94
- integrate.console.print(f"[yellow]{name} CLI not found on PATH. Skipping optional integration.[/yellow]")
95
- add_result(name, True, None, "CLI not found; skipped optional global/client registration.")
96
- return
97
- code = integrate._run(command)
98
- add_result(name, code == 0, None, "MCP registration command exited " + str(code))
99
-
100
- def apply_project_file(name: str) -> None:
101
- writers = {
102
- "cursor": integrate._write_cursor_config,
103
- "opencode": integrate._write_opencode_config,
104
- "antigravity": integrate._write_antigravity_mcp_config,
105
- "warp": integrate._write_warp_mcp_config,
106
- }
107
- recorders = {
108
- "cursor": integrate._record_cursor_config,
109
- "opencode": integrate._record_opencode_config,
110
- "antigravity": integrate._record_antigravity_config,
111
- "warp": integrate._record_warp_config,
112
- }
113
- path = writers[name](root)
114
- recorders[name](root)
115
- add_result(name, True, path, "Project integration config written.")
116
-
117
- def apply_aider() -> None:
118
- integrate._record_aider_config(root)
119
- add_result("aider", True, root / ".devcouncil" / "config.yaml", "Aider executor enabled.")
120
-
121
- def apply_hooks() -> None:
122
- integrate._configure_native_hooks(root, "all", apply=True)
123
- add_result("hooks", True, None, "Native hook files configured.")
124
-
125
- if normalized == "all":
126
- for name in ("codex", "gemini", "claude"):
127
- apply_first_party(name)
128
- # Batch the per-tool config.yaml record updates into one load/save.
129
- with integrate._batched_raw_config(root):
130
- for name in ("cursor", "opencode", "antigravity", "warp"):
131
- apply_project_file(name)
132
- apply_aider()
133
- if include_hooks:
134
- apply_hooks()
135
- elif normalized in {"codex", "gemini", "claude"}:
136
- apply_first_party(normalized)
137
- elif normalized in {"cursor", "opencode", "antigravity", "warp"}:
138
- apply_project_file(normalized)
139
- elif normalized == "aider":
140
- apply_aider()
141
- elif normalized == "hooks":
142
- apply_hooks()
143
-
144
- check = build_integration_check_report(root, strict=strict).as_dict()
145
- ok = all(item["ok"] for item in results) and (not strict or bool(check["ok"]))
146
- return IntegrationActionReport(normalized, ok, results, warnings, check)