devcouncil 0.1.1 → 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 (129) hide show
  1. package/README.md +190 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +167 -5
  5. package/src/devcouncil/artifacts/graph.py +23 -3
  6. package/src/devcouncil/assets/__init__.py +1 -0
  7. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  8. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  9. package/src/devcouncil/cli/commands/agents.py +292 -0
  10. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  11. package/src/devcouncil/cli/commands/check.py +209 -0
  12. package/src/devcouncil/cli/commands/config.py +43 -4
  13. package/src/devcouncil/cli/commands/cost.py +57 -0
  14. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  15. package/src/devcouncil/cli/commands/doctor.py +221 -21
  16. package/src/devcouncil/cli/commands/evidence.py +48 -0
  17. package/src/devcouncil/cli/commands/go.py +452 -33
  18. package/src/devcouncil/cli/commands/handoff.py +69 -0
  19. package/src/devcouncil/cli/commands/hook.py +124 -15
  20. package/src/devcouncil/cli/commands/init.py +154 -18
  21. package/src/devcouncil/cli/commands/integrate.py +894 -105
  22. package/src/devcouncil/cli/commands/map.py +80 -10
  23. package/src/devcouncil/cli/commands/plan.py +212 -51
  24. package/src/devcouncil/cli/commands/prompt.py +18 -7
  25. package/src/devcouncil/cli/commands/repair.py +40 -23
  26. package/src/devcouncil/cli/commands/report.py +8 -0
  27. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  28. package/src/devcouncil/cli/commands/rollback.py +27 -28
  29. package/src/devcouncil/cli/commands/run.py +69 -49
  30. package/src/devcouncil/cli/commands/runs.py +223 -0
  31. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  32. package/src/devcouncil/cli/commands/semantic.py +47 -0
  33. package/src/devcouncil/cli/commands/setup.py +145 -6
  34. package/src/devcouncil/cli/commands/shell.py +73 -0
  35. package/src/devcouncil/cli/commands/skills.py +88 -0
  36. package/src/devcouncil/cli/commands/status.py +25 -1
  37. package/src/devcouncil/cli/commands/trace.py +47 -3
  38. package/src/devcouncil/cli/commands/verify.py +138 -3
  39. package/src/devcouncil/cli/commands/watch.py +9 -9
  40. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  41. package/src/devcouncil/cli/main.py +56 -7
  42. package/src/devcouncil/domain/evidence.py +22 -2
  43. package/src/devcouncil/domain/gap.py +27 -1
  44. package/src/devcouncil/domain/task.py +31 -2
  45. package/src/devcouncil/execution/checkpoints.py +246 -0
  46. package/src/devcouncil/execution/context_builder.py +1 -1
  47. package/src/devcouncil/execution/fs_watcher.py +180 -0
  48. package/src/devcouncil/execution/handoff.py +102 -0
  49. package/src/devcouncil/execution/hook_policy.py +162 -74
  50. package/src/devcouncil/execution/patch.py +59 -10
  51. package/src/devcouncil/execution/permissions.py +17 -24
  52. package/src/devcouncil/execution/policy_engine.py +343 -0
  53. package/src/devcouncil/execution/prompt_builder.py +633 -21
  54. package/src/devcouncil/execution/shell_session.py +225 -0
  55. package/src/devcouncil/execution/task_runner.py +6 -2
  56. package/src/devcouncil/executors/agent_registry.py +575 -0
  57. package/src/devcouncil/executors/coding_cli.py +663 -39
  58. package/src/devcouncil/executors/native/agent.py +121 -20
  59. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  60. package/src/devcouncil/gating/checks/secret_scan_check.py +40 -21
  61. package/src/devcouncil/gating/policy.py +158 -10
  62. package/src/devcouncil/hardware.py +184 -0
  63. package/src/devcouncil/indexing/ast_matcher.py +1 -1
  64. package/src/devcouncil/indexing/lsp.py +45 -4
  65. package/src/devcouncil/indexing/repo_mapper.py +1256 -9
  66. package/src/devcouncil/indexing/semantic_index.py +205 -0
  67. package/src/devcouncil/integrations/actions.py +146 -0
  68. package/src/devcouncil/integrations/check.py +423 -0
  69. package/src/devcouncil/integrations/github_intent.py +142 -0
  70. package/src/devcouncil/integrations/gitnexus.py +35 -0
  71. package/src/devcouncil/integrations/mcp/server.py +1552 -29
  72. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  73. package/src/devcouncil/live/cards.py +161 -19
  74. package/src/devcouncil/live/signals.py +2 -2
  75. package/src/devcouncil/live/transcripts.py +9 -6
  76. package/src/devcouncil/llm/cache.py +10 -6
  77. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  78. package/src/devcouncil/llm/provider.py +515 -34
  79. package/src/devcouncil/llm/router.py +231 -46
  80. package/src/devcouncil/optimization/__init__.py +1 -0
  81. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  82. package/src/devcouncil/planning/correction_manifest.py +303 -0
  83. package/src/devcouncil/planning/critique_service.py +7 -2
  84. package/src/devcouncil/planning/plan_service.py +17 -3
  85. package/src/devcouncil/planning/prompt_enhancer_service.py +82 -1
  86. package/src/devcouncil/planning/spec_service.py +27 -1
  87. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  88. package/src/devcouncil/repo/gitignore.py +123 -0
  89. package/src/devcouncil/repo/sca.py +374 -0
  90. package/src/devcouncil/reporting/json_report.py +11 -1
  91. package/src/devcouncil/reporting/markdown_report.py +15 -0
  92. package/src/devcouncil/skills/__init__.py +19 -0
  93. package/src/devcouncil/skills/library/README.md +46 -0
  94. package/src/devcouncil/skills/library/ai-training.md +50 -0
  95. package/src/devcouncil/skills/library/android.md +50 -0
  96. package/src/devcouncil/skills/library/backend.md +52 -0
  97. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  98. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  99. package/src/devcouncil/skills/library/desktop.md +46 -0
  100. package/src/devcouncil/skills/library/devops.md +48 -0
  101. package/src/devcouncil/skills/library/game-dev.md +46 -0
  102. package/src/devcouncil/skills/library/ios.md +48 -0
  103. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  104. package/src/devcouncil/skills/library/security.md +48 -0
  105. package/src/devcouncil/skills/library/systems.md +48 -0
  106. package/src/devcouncil/skills/library/web.md +47 -0
  107. package/src/devcouncil/skills/library/windows.md +47 -0
  108. package/src/devcouncil/skills/registry.py +330 -0
  109. package/src/devcouncil/storage/db.py +83 -2
  110. package/src/devcouncil/storage/models.py +121 -0
  111. package/src/devcouncil/storage/native.py +557 -0
  112. package/src/devcouncil/storage/repositories.py +137 -75
  113. package/src/devcouncil/telemetry/cost.py +123 -17
  114. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  115. package/src/devcouncil/telemetry/pricing.py +28 -0
  116. package/src/devcouncil/telemetry/traces.py +62 -7
  117. package/src/devcouncil/telemetry/tracker.py +12 -9
  118. package/src/devcouncil/ui/dashboard.py +324 -23
  119. package/src/devcouncil/utils/redaction.py +9 -3
  120. package/src/devcouncil/utils/subprocess_env.py +69 -0
  121. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  122. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  123. package/src/devcouncil/verification/diff_coverage.py +353 -0
  124. package/src/devcouncil/verification/next_actions.py +189 -0
  125. package/src/devcouncil/verification/sandbox.py +178 -0
  126. package/src/devcouncil/verification/test_resolver.py +91 -0
  127. package/src/devcouncil/verification/verifier.py +1065 -47
  128. package/uv.lock +205 -64
  129. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -23,13 +23,17 @@ def _project_root(project_root: Path | None = None) -> Path:
23
23
 
24
24
 
25
25
  def _active_task(root: Path):
26
+ # Resolve the *single* unambiguous running task. active_task_id returns None when
27
+ # zero or multiple tasks are running, so we never authorize a write against the
28
+ # wrong task; the policy engine then denies for task=None (fail-closed).
29
+ active_id = active_task_id(root)
30
+ if not active_id:
31
+ return None
26
32
  db = get_db(root)
27
33
  if not db:
28
34
  return None
29
35
  with db.get_session() as session:
30
- task_repo = TaskRepository(session)
31
- running_tasks = [t for t in task_repo.get_all() if t.status == "running"]
32
- return running_tasks[0] if running_tasks else None
36
+ return TaskRepository(session).get_by_id(active_id)
33
37
 
34
38
 
35
39
  def _emit_decision(client: str, action: str, reason: str) -> None:
@@ -48,31 +52,53 @@ def _emit_decision(client: str, action: str, reason: str) -> None:
48
52
  console.print(f"[yellow]DevCouncil Warning:[/yellow] {reason}")
49
53
 
50
54
 
55
+ def _emit_unevaluable(client: str, reason: str, strict: bool, *, action: str = "warn") -> None:
56
+ """Decide what to do when a tool call cannot be evaluated (empty/malformed/error).
57
+
58
+ Fail-closed in strict mode (block), otherwise surface a warning but allow — and
59
+ never leak an undefined exit code, which would silently disable the only pre-action
60
+ gate."""
61
+ _emit_decision(client, "deny" if strict else action, f"{reason}{' (strict mode: blocking)' if strict else ''}")
62
+
63
+
51
64
  @app.command()
52
65
  def pre_tool_use(
53
66
  tool_call_json: str | None = typer.Argument(None, help="The JSON string of the tool call from the coding CLI."),
54
67
  client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
55
68
  project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
69
+ strict: bool = typer.Option(
70
+ False,
71
+ "--strict",
72
+ envvar="DEVCOUNCIL_HOOK_STRICT",
73
+ help="Fail closed (block) when a tool call cannot be parsed or evaluated.",
74
+ ),
56
75
  ):
57
76
  """
58
77
  Coding CLI hook: Inspects a tool call before execution.
59
78
  Exits with code 2 to block unauthorized file writes.
60
79
  """
80
+ normalized_client = client.lower()
61
81
  try:
62
82
  if tool_call_json is None:
63
83
  tool_call_json = sys.stdin.read()
84
+ # Empty payload: nothing to evaluate. Benign in normal use, so allow — but make
85
+ # it observable, and block under --strict.
64
86
  if not tool_call_json.strip():
65
- raise typer.Exit(code=0)
66
- call_data = json.loads(tool_call_json)
67
- normalized_client = client.lower()
87
+ return _emit_unevaluable(normalized_client, "Empty tool-call payload; nothing to evaluate.", strict, action="allow")
88
+ try:
89
+ call_data = json.loads(tool_call_json)
90
+ except json.JSONDecodeError:
91
+ # A real tool call we cannot parse must not silently pass the gate.
92
+ return _emit_unevaluable(normalized_client, "Tool-call payload was not valid JSON; could not enforce policy.", strict)
68
93
  root = _project_root(project_root)
69
94
  active_task = _active_task(root)
70
95
 
71
96
  decision = HookPolicy(project_root=root).evaluate(call_data, active_task)
72
97
  _emit_decision(normalized_client, decision.action, decision.reason)
73
-
74
- except json.JSONDecodeError:
75
- raise typer.Exit(code=0)
98
+ except typer.Exit:
99
+ raise
100
+ except Exception as exc: # never emit an undefined exit code from a crashing hook
101
+ return _emit_unevaluable(normalized_client, f"Hook error: {exc}", strict)
76
102
 
77
103
  @app.command()
78
104
  def post_tool_use(
@@ -117,12 +143,95 @@ def agent_response(
117
143
  print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
118
144
 
119
145
  @app.command()
120
- def post_task():
146
+ def post_task(
147
+ client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
148
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
149
+ ):
121
150
  """
122
151
  Coding CLI hook: Runs after a task is completed.
123
- Triggers deterministic verification.
152
+
153
+ When ``execution.verify_on_post_task`` is enabled, this runs deterministic
154
+ verification of the active task and records gaps; otherwise it just reminds the
155
+ user to run ``dev verify`` (the default, to keep hooks fast/cheap).
124
156
  """
125
- console.print("[cyan]DevCouncil: coding agent finished task. Triggering automatic verification...[/cyan]")
126
- # In a real environment, this would invoke 'dev verify <active-task>'
127
- # For the hook script, we just notify the user.
128
- console.print("Run [bold]dev verify[/bold] to finalize implementation evidence.")
157
+ root = _project_root(project_root)
158
+ try:
159
+ from devcouncil.app.config import load_config
160
+ verify_enabled = load_config(root).execution.verify_on_post_task
161
+ except Exception:
162
+ verify_enabled = False
163
+
164
+ if not verify_enabled:
165
+ console.print("[cyan]DevCouncil: coding agent finished task.[/cyan]")
166
+ console.print("Run [bold]dev verify[/bold] to finalize implementation evidence.")
167
+ _emit_post_task_allow(client)
168
+ return
169
+
170
+ summary = _verify_active_task(root)
171
+ console.print(summary)
172
+ _emit_post_task_allow(client)
173
+
174
+
175
+ def _emit_post_task_allow(client: str) -> None:
176
+ if client.lower() in {"codex", "gemini"}:
177
+ print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
178
+
179
+
180
+ def _verify_active_task(root: Path) -> str:
181
+ """Run deterministic verification of the active task and persist gaps/evidence.
182
+ Returns a human summary line. Best-effort: never raises out of a hook."""
183
+ try:
184
+ import asyncio
185
+
186
+ from devcouncil.domain.evidence import CommandResult, DiffCoverageEvidence, DiffEvidence, TestEvidence
187
+ from devcouncil.storage.repositories import (
188
+ EvidenceRepository,
189
+ GapRepository,
190
+ RequirementRepository,
191
+ )
192
+ from devcouncil.verification.next_actions import split_next_actions
193
+ from devcouncil.verification.verifier import Verifier
194
+
195
+ active_id = active_task_id(root)
196
+ db = get_db(root)
197
+ if not active_id or not db:
198
+ return "Run [bold]dev verify[/bold] to finalize implementation evidence."
199
+ with db.get_session() as session:
200
+ task = TaskRepository(session).get_by_id(active_id)
201
+ if not task:
202
+ return "Run [bold]dev verify[/bold] to finalize implementation evidence."
203
+ reqs = RequirementRepository(session).get_all()
204
+ gaps, evidence = asyncio.run(Verifier(root).verify_task(task, reqs))
205
+ gap_repo = GapRepository(session)
206
+ ev_repo = EvidenceRepository(session)
207
+ gap_repo.delete_for_task(task.id)
208
+ ev_repo.delete_for_task(task.id)
209
+ for gap in gaps:
210
+ gap_repo.save(gap)
211
+ for ev in evidence:
212
+ if isinstance(ev, CommandResult):
213
+ ev_repo.save_command_result(task.id, ev)
214
+ elif isinstance(ev, DiffCoverageEvidence):
215
+ ev_repo.save_diff_coverage_evidence(ev)
216
+ elif isinstance(ev, DiffEvidence):
217
+ ev_repo.save_diff_evidence(ev)
218
+ elif isinstance(ev, TestEvidence):
219
+ ev_repo.save_test_evidence(ev, task.id)
220
+ blocking = [g for g in gaps if g.blocking]
221
+ task.status = "blocked" if blocking else "verified"
222
+ TaskRepository(session).save(task)
223
+ blocking_actions, _ = split_next_actions(gaps)
224
+ TraceLogger(root).log_event(
225
+ "post_task_verified",
226
+ {"task_id": active_id, "blocking": len(blocking)},
227
+ task_id=active_id,
228
+ summary=f"post_task verification: {task.status}",
229
+ )
230
+ if blocking:
231
+ return (
232
+ f"[yellow]{active_id} is blocked by {len(blocking)} gap(s); "
233
+ f"{len(blocking_actions)} next action(s). Run [bold]dev repair[/bold].[/yellow]"
234
+ )
235
+ return f"[green]{active_id} verified.[/green]"
236
+ except Exception as exc: # never let a hook crash the agent
237
+ return f"[dim]post-task verification skipped: {exc}[/dim]"
@@ -1,4 +1,5 @@
1
1
  import copy
2
+ from typing import Any
2
3
  import typer
3
4
  import yaml
4
5
  from rich.console import Console
@@ -6,10 +7,42 @@ from pathlib import Path
6
7
  from devcouncil.storage.db import Database
7
8
  from devcouncil.integrations.gitnexus import GitNexusIntegration
8
9
  from devcouncil.integrations.graphify import GraphifyIntegration
10
+ from devcouncil.llm.provider import build_role_model_config, validate_model_provider
11
+ from devcouncil.repo.gitignore import ensure_gitignore
9
12
 
10
13
  app = typer.Typer()
11
14
  console = Console()
12
15
 
16
+ # Per-stack default verification commands. A fresh project gets ONLY the commands
17
+ # for the stack(s) actually detected in the repo, so the verifier never inherits a
18
+ # cross-stack gate (e.g. `npm test`/`eslint`/`tsc` on a Python repo) that it would run
19
+ # as a blocking fallback and fail for tooling/stack reasons instead of a real defect.
20
+ _STACK_COMMAND_DEFAULTS: dict[str, dict[str, list[str]]] = {
21
+ "python": {"test": ["pytest"], "lint": ["ruff check ."], "typecheck": ["mypy ."]},
22
+ "node": {"test": ["npm test"], "lint": ["eslint ."], "typecheck": ["tsc --noEmit"]},
23
+ }
24
+
25
+
26
+ def _stack_aware_commands(project_root: Path) -> dict[str, list[str]]:
27
+ """Default test/lint/typecheck commands scoped to the repo's detected stack(s).
28
+
29
+ Returns empty lists when no stack is detected — empty is safe (no speculative
30
+ fallback gates) and far better than guessing wrong-stack tools."""
31
+ from devcouncil.repo.ci_scaffold import detect_stacks
32
+
33
+ commands: dict[str, list[str]] = {"test": [], "lint": [], "typecheck": []}
34
+ try:
35
+ stacks = detect_stacks(project_root)
36
+ except Exception:
37
+ return commands
38
+ for stack in sorted(stacks):
39
+ for key, cmds in _STACK_COMMAND_DEFAULTS.get(stack, {}).items():
40
+ for command in cmds:
41
+ if command not in commands[key]:
42
+ commands[key].append(command)
43
+ return commands
44
+
45
+
13
46
  DEFAULT_CONFIG = {
14
47
  "project": {
15
48
  "name": "devcouncil-project",
@@ -18,23 +51,12 @@ DEFAULT_CONFIG = {
18
51
  },
19
52
  "models": {
20
53
  "provider": "openrouter",
21
- "roles": {
22
- "spec_writer": {"model": "anthropic/claude-3.5-sonnet"},
23
- "prompt_enhancer": {"model": "anthropic/claude-3.5-sonnet"},
24
- "planner_a": {"model": "anthropic/claude-3.5-sonnet"},
25
- "planner_b": {"model": "google/gemini-pro-1.5"},
26
- "critic_a": {"model": "openai/gpt-4o"},
27
- "critic_b": {"model": "anthropic/claude-3-opus"},
28
- "arbiter": {"model": "openai/gpt-4o"},
29
- "native_agent": {"model": "anthropic/claude-3.5-sonnet"},
30
- "implementation_reviewer": {"model": "openai/gpt-4o"},
31
- "live_reviewer": {"model": "openai/gpt-4o"},
32
- }
54
+ "roles": build_role_model_config("openrouter"),
33
55
  },
34
56
  "commands": {
35
- "test": ["pytest", "npm test"],
36
- "lint": ["flake8", "eslint"],
37
- "typecheck": ["mypy", "tsc"]
57
+ "test": [],
58
+ "lint": [],
59
+ "typecheck": [],
38
60
  },
39
61
  "gates": {
40
62
  "require_clean_git_before_task": True,
@@ -45,9 +67,12 @@ DEFAULT_CONFIG = {
45
67
  "block_failed_commands": True
46
68
  },
47
69
  "execution": {
48
- "default_executor": "native",
70
+ "default_executor": "manual",
49
71
  "max_repair_attempts": 3,
50
- "checkpoint_before_each_task": True
72
+ "checkpoint_before_each_task": True,
73
+ "stream_cli_output": False,
74
+ "cursor_resume_mode": "off",
75
+ "coding_cli_probe_order": [],
51
76
  },
52
77
  "privacy": {
53
78
  "redact_env_vars": True,
@@ -71,15 +96,90 @@ DEFAULT_CONFIG = {
71
96
  "signals_path": ".devcouncil/live/signals",
72
97
  "default_client": "claude",
73
98
  },
99
+ "cli_agents": {
100
+ "enabled": True,
101
+ "profiles": {
102
+ "default": {
103
+ "description": "Balanced local execution with DevCouncil verification.",
104
+ },
105
+ "yolo": {
106
+ "description": "Faster local execution; DevCouncil still verifies the final diff.",
107
+ "timeout_seconds": 3600,
108
+ "prompt_preamble": "Profile: yolo. Move efficiently within the task scope.",
109
+ },
110
+ "prod": {
111
+ "description": "Restrictive execution for high-risk repositories.",
112
+ "timeout_seconds": 1800,
113
+ "prompt_preamble": "Profile: prod. Keep edits minimal and explicitly within task scope.",
114
+ "require_explicit_confirmation": True,
115
+ },
116
+ },
117
+ "agents": {},
118
+ },
74
119
  }
75
120
  }
76
121
 
77
122
 
123
+ def parse_role_model_overrides(values: list[str] | None) -> dict[str, str]:
124
+ overrides: dict[str, str] = {}
125
+ for value in values or []:
126
+ if "=" not in value:
127
+ raise ValueError(f"Invalid --role-model value '{value}'. Use ROLE=MODEL.")
128
+ role, model = value.split("=", 1)
129
+ role = role.strip()
130
+ model = model.strip()
131
+ if not role or not model:
132
+ raise ValueError(f"Invalid --role-model value '{value}'. Use ROLE=MODEL.")
133
+ overrides[role] = model
134
+ return overrides
135
+
136
+
137
+ def _generate_initial_map(project_root: Path, quiet: bool) -> None:
138
+ """Best-effort repo map + agent guide generation on fresh init.
139
+
140
+ Imported lazily to avoid a circular import with the map command, and wrapped
141
+ so a mapping failure never blocks initialization.
142
+ """
143
+ try:
144
+ from devcouncil.cli.commands.map import generate_map_artifacts
145
+
146
+ generate_map_artifacts(project_root, project_root / ".devcouncil" / "repo_map.json")
147
+ if not quiet:
148
+ console.print("[green]Generated .devcouncil/repo_map.json and agent guides (AGENTS.md, CLAUDE.md).[/green]")
149
+ except Exception as exc: # mapping is best-effort, never fatal
150
+ if not quiet:
151
+ console.print(f"[yellow]Skipped repo map generation: {exc}. Run 'dev map' later.[/yellow]")
152
+
153
+
154
+ def _scaffold_initial_skills(project_root: Path, quiet: bool) -> None:
155
+ """Best-effort scaffolding of applicable engineering skills into .claude/skills/.
156
+
157
+ Always writes the core-engineering skill; adds domain skills (android, ios, web,
158
+ ...) whose file triggers match the repository. Never fatal.
159
+ """
160
+ try:
161
+ from devcouncil.skills.registry import scaffold_skills, select_skills
162
+
163
+ selected = select_skills(project_root=project_root)
164
+ written = scaffold_skills(project_root, selected)
165
+ if written and not quiet:
166
+ names = ", ".join(sorted(skill.name for skill in selected))
167
+ console.print(f"[green]Scaffolded {len(written)} skill(s) into .claude/skills/ ({names}).[/green]")
168
+ except Exception as exc: # skill scaffolding is best-effort, never fatal
169
+ if not quiet:
170
+ console.print(f"[yellow]Skipped skill scaffolding: {exc}. Run 'dev skills scaffold' later.[/yellow]")
171
+
172
+
78
173
  def initialize_project(
79
174
  project_root: Path = Path("."),
80
175
  project_name: str | None = None,
176
+ model_provider: str = "openrouter",
177
+ model: str | None = None,
178
+ role_models: dict[str, str] | None = None,
81
179
  with_gitnexus: bool = False,
82
180
  with_graphify: bool = False,
181
+ with_map: bool = True,
182
+ with_skills: bool = True,
83
183
  quiet: bool = False,
84
184
  ) -> bool:
85
185
  """Initialize DevCouncil project state.
@@ -100,11 +200,20 @@ def initialize_project(
100
200
  (dev_dir / "logs").mkdir(exist_ok=True)
101
201
 
102
202
  config_path = dev_dir / "config.yaml"
103
- config = copy.deepcopy(DEFAULT_CONFIG)
203
+ config: dict[str, Any] = copy.deepcopy(DEFAULT_CONFIG)
204
+ # Scope default verification commands to the repo's actual stack(s).
205
+ config["commands"] = _stack_aware_commands(project_root)
104
206
  if project_name:
105
207
  config["project"]["name"] = project_name
106
208
  else:
107
209
  config["project"]["name"] = project_root.name
210
+ provider = validate_model_provider(model_provider)
211
+ config["models"]["provider"] = provider
212
+ config["models"]["roles"] = build_role_model_config(
213
+ provider,
214
+ model=model,
215
+ role_models=role_models,
216
+ )
108
217
 
109
218
  with open(config_path, "w") as f:
110
219
  yaml.dump(config, f, default_flow_style=False)
@@ -115,6 +224,11 @@ def initialize_project(
115
224
  console.print(f"[green]Successfully initialized DevCouncil in {dev_dir}[/green]")
116
225
  created = True
117
226
 
227
+ if with_map:
228
+ _generate_initial_map(project_root, quiet)
229
+ if with_skills:
230
+ _scaffold_initial_skills(project_root, quiet)
231
+
118
232
  if with_gitnexus:
119
233
  nexus = GitNexusIntegration(project_root)
120
234
  nexus.initialize()
@@ -123,6 +237,7 @@ def initialize_project(
123
237
  graphify = GraphifyIntegration(project_root)
124
238
  graphify.initialize()
125
239
 
240
+ ensure_gitignore(project_root)
126
241
  return created
127
242
 
128
243
 
@@ -130,8 +245,17 @@ def initialize_project(
130
245
  def init(
131
246
  ctx: typer.Context,
132
247
  project_name: str = typer.Option(None, "--name", "-n", help="Project name"),
248
+ provider: str = typer.Option("openrouter", "--provider", help="Model provider for generated config."),
249
+ model: str | None = typer.Option(None, "--model", "-m", help="Model id to use for every default role."),
250
+ role_model: list[str] | None = typer.Option(
251
+ None,
252
+ "--role-model",
253
+ help="Per-role model override in ROLE=MODEL form. Can be repeated.",
254
+ ),
133
255
  with_gitnexus: bool = typer.Option(False, "--gitnexus", help="Initialize GitNexus structural awareness"),
134
256
  with_graphify: bool = typer.Option(False, "--graphify", help="Initialize Graphify knowledge graph engine"),
257
+ skip_map: bool = typer.Option(False, "--skip-map", help="Skip generating repo_map.json and agent guides on init."),
258
+ skip_skills: bool = typer.Option(False, "--skip-skills", help="Skip scaffolding engineering skills into .claude/skills/ on init."),
135
259
  ):
136
260
  """
137
261
  Initialize DevCouncil in the current directory.
@@ -145,9 +269,21 @@ def init(
145
269
  console.print("Use --gitnexus or --graphify to add upgrade paths.")
146
270
  raise typer.Exit()
147
271
 
272
+ try:
273
+ role_models = parse_role_model_overrides(role_model)
274
+ model_provider = validate_model_provider(provider)
275
+ except ValueError as e:
276
+ console.print(f"[red]{e}[/red]")
277
+ raise typer.Exit(code=2) from e
278
+
148
279
  initialize_project(
149
280
  Path("."),
150
281
  project_name=project_name,
282
+ model_provider=model_provider,
283
+ model=model,
284
+ role_models=role_models,
151
285
  with_gitnexus=with_gitnexus,
152
286
  with_graphify=with_graphify,
287
+ with_map=not skip_map,
288
+ with_skills=not skip_skills,
153
289
  )