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
@@ -0,0 +1,205 @@
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)
@@ -0,0 +1,146 @@
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)