devcouncil 0.2.0 → 0.3.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 (98) hide show
  1. package/README.md +12 -1
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/src/devcouncil/app/config.py +181 -7
  5. package/src/devcouncil/app/orchestrator.py +10 -6
  6. package/src/devcouncil/app/state_machine.py +4 -0
  7. package/src/devcouncil/artifacts/graph.py +9 -2
  8. package/src/devcouncil/cli/commands/check.py +12 -1
  9. package/src/devcouncil/cli/commands/design.py +186 -0
  10. package/src/devcouncil/cli/commands/doctor.py +160 -3
  11. package/src/devcouncil/cli/commands/go.py +96 -16
  12. package/src/devcouncil/cli/commands/hook.py +172 -0
  13. package/src/devcouncil/cli/commands/init.py +7 -2
  14. package/src/devcouncil/cli/commands/integrate.py +492 -34
  15. package/src/devcouncil/cli/commands/logs.py +106 -0
  16. package/src/devcouncil/cli/commands/okf.py +245 -0
  17. package/src/devcouncil/cli/commands/plan.py +54 -14
  18. package/src/devcouncil/cli/commands/repair.py +12 -3
  19. package/src/devcouncil/cli/commands/run.py +128 -7
  20. package/src/devcouncil/cli/commands/skills.py +180 -1
  21. package/src/devcouncil/cli/commands/status.py +7 -16
  22. package/src/devcouncil/cli/commands/verify.py +16 -10
  23. package/src/devcouncil/cli/commands/watch.py +24 -4
  24. package/src/devcouncil/cli/main.py +36 -1
  25. package/src/devcouncil/domain/evidence.py +7 -0
  26. package/src/devcouncil/execution/checkpoints.py +12 -2
  27. package/src/devcouncil/execution/fs_watcher.py +27 -2
  28. package/src/devcouncil/execution/handoff.py +1 -1
  29. package/src/devcouncil/execution/patch.py +6 -0
  30. package/src/devcouncil/execution/permissions.py +7 -0
  31. package/src/devcouncil/execution/policy_engine.py +12 -5
  32. package/src/devcouncil/execution/prompt_builder.py +126 -10
  33. package/src/devcouncil/execution/shell_session.py +6 -0
  34. package/src/devcouncil/execution/task_runner.py +18 -7
  35. package/src/devcouncil/executors/agent_registry.py +22 -1
  36. package/src/devcouncil/executors/coding_cli.py +133 -5
  37. package/src/devcouncil/executors/mini_swe.py +6 -0
  38. package/src/devcouncil/executors/native/agent.py +15 -0
  39. package/src/devcouncil/executors/openhands.py +6 -0
  40. package/src/devcouncil/gating/checks/secret_scan_check.py +7 -0
  41. package/src/devcouncil/gating/policy.py +38 -7
  42. package/src/devcouncil/indexing/ast_matcher.py +16 -6
  43. package/src/devcouncil/indexing/repo_mapper.py +30 -8
  44. package/src/devcouncil/indexing/semantic_index.py +42 -26
  45. package/src/devcouncil/integrations/actions.py +24 -4
  46. package/src/devcouncil/integrations/check.py +7 -4
  47. package/src/devcouncil/integrations/claude_assets.py +444 -0
  48. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  49. package/src/devcouncil/integrations/github_intent.py +8 -1
  50. package/src/devcouncil/integrations/gitnexus.py +10 -2
  51. package/src/devcouncil/integrations/mcp/server.py +404 -15
  52. package/src/devcouncil/integrations/pr_comments.py +9 -0
  53. package/src/devcouncil/knowledge/__init__.py +23 -0
  54. package/src/devcouncil/knowledge/design.py +374 -0
  55. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  56. package/src/devcouncil/knowledge/fetch.py +223 -0
  57. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  58. package/src/devcouncil/knowledge/okf.py +202 -0
  59. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  60. package/src/devcouncil/knowledge/sources.py +239 -0
  61. package/src/devcouncil/live/cards.py +20 -6
  62. package/src/devcouncil/live/repair_prompt.py +29 -6
  63. package/src/devcouncil/live/reviewer.py +72 -13
  64. package/src/devcouncil/live/summary.py +18 -8
  65. package/src/devcouncil/live/transcripts.py +38 -5
  66. package/src/devcouncil/llm/cache.py +14 -6
  67. package/src/devcouncil/llm/provider.py +179 -92
  68. package/src/devcouncil/llm/router.py +122 -23
  69. package/src/devcouncil/optimization/skillopt.py +673 -0
  70. package/src/devcouncil/planning/arbiter_service.py +10 -2
  71. package/src/devcouncil/planning/correction_manifest.py +47 -4
  72. package/src/devcouncil/planning/critique_service.py +9 -2
  73. package/src/devcouncil/planning/plan_service.py +69 -3
  74. package/src/devcouncil/planning/prompt_enhancer_service.py +124 -0
  75. package/src/devcouncil/planning/repair_service.py +8 -2
  76. package/src/devcouncil/planning/spec_service.py +10 -2
  77. package/src/devcouncil/repo/ci_scaffold.py +13 -5
  78. package/src/devcouncil/repo/sca.py +11 -1
  79. package/src/devcouncil/reporting/json_report.py +11 -0
  80. package/src/devcouncil/reporting/markdown_report.py +14 -1
  81. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  82. package/src/devcouncil/reporting/okf_html.py +323 -0
  83. package/src/devcouncil/reporting/report_builder.py +18 -1
  84. package/src/devcouncil/skills/registry.py +111 -33
  85. package/src/devcouncil/storage/db.py +58 -2
  86. package/src/devcouncil/storage/models.py +4 -0
  87. package/src/devcouncil/storage/native.py +20 -18
  88. package/src/devcouncil/storage/repositories.py +35 -18
  89. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  90. package/src/devcouncil/telemetry/stages.py +141 -0
  91. package/src/devcouncil/telemetry/tracker.py +12 -1
  92. package/src/devcouncil/ui/dashboard.py +69 -5
  93. package/src/devcouncil/verification/acceptance_compiler.py +147 -19
  94. package/src/devcouncil/verification/ad_hoc_check.py +6 -0
  95. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  96. package/src/devcouncil/verification/sandbox.py +7 -4
  97. package/src/devcouncil/verification/verifier.py +905 -517
  98. package/uv.lock +1 -1
@@ -142,6 +142,178 @@ def agent_response(
142
142
  if client.lower() in {"codex", "gemini"}:
143
143
  print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
144
144
 
145
+ def _status_line(root: Path) -> str | None:
146
+ """A one-line DevCouncil status snapshot, or None when uninitialized/unavailable.
147
+
148
+ Used by the SessionStart and UserPromptSubmit hooks to inject lightweight project
149
+ context into Claude Code. Best-effort: any failure returns None so a hook never
150
+ breaks the session."""
151
+ try:
152
+ db = get_db(root)
153
+ if not db:
154
+ return None
155
+ from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository
156
+ from devcouncil.app.project_status import compute_phase
157
+
158
+ with db.get_session() as session:
159
+ graph = ArtifactGraphRepository(session).load_graph()
160
+ summary = graph.coverage_summary()
161
+ state = StateRepository(session).get_state()
162
+ phase = compute_phase(graph, state.current_phase if state else None)
163
+ return (
164
+ f"DevCouncil — phase: {phase}; tasks: {summary['total_tasks']}; "
165
+ f"gaps: {summary['total_gaps']} ({summary['blocking_gaps']} blocking). "
166
+ "Use the devcouncil_* MCP tools and `dev` CLI to stay inside the verify loop."
167
+ )
168
+ except Exception:
169
+ return None
170
+
171
+
172
+ def _emit_additional_context(event_name: str, context: str | None) -> None:
173
+ """Emit a Claude-Code hook result that injects additionalContext (exit 0)."""
174
+ if not context:
175
+ return
176
+ print(json.dumps({
177
+ "hookSpecificOutput": {
178
+ "hookEventName": event_name,
179
+ "additionalContext": context,
180
+ }
181
+ }, separators=(",", ":")))
182
+
183
+
184
+ def _read_stdin_payload(event_json: str | None) -> dict:
185
+ text = event_json if event_json is not None else sys.stdin.read()
186
+ if not text or not text.strip():
187
+ return {}
188
+ try:
189
+ parsed = json.loads(text)
190
+ return parsed if isinstance(parsed, dict) else {"raw": text}
191
+ except json.JSONDecodeError:
192
+ return {"raw": text}
193
+
194
+
195
+ @app.command()
196
+ def session_start(
197
+ event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
198
+ client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
199
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
200
+ ):
201
+ """Claude Code SessionStart hook: inject a DevCouncil status snapshot as context."""
202
+ _read_stdin_payload(event_json)
203
+ root = _project_root(project_root)
204
+ try:
205
+ TraceLogger(root).log_event("session_start", {"client": client.lower()}, summary="Claude session started.")
206
+ except Exception:
207
+ pass
208
+ _emit_additional_context("SessionStart", _status_line(root))
209
+
210
+
211
+ @app.command()
212
+ def user_prompt_submit(
213
+ event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
214
+ client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
215
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
216
+ ):
217
+ """Claude Code UserPromptSubmit hook: surface the current DevCouncil status as context."""
218
+ _read_stdin_payload(event_json)
219
+ root = _project_root(project_root)
220
+ _emit_additional_context("UserPromptSubmit", _status_line(root))
221
+
222
+
223
+ @app.command()
224
+ def session_end(
225
+ event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
226
+ client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
227
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
228
+ ):
229
+ """Claude Code SessionEnd hook: record session teardown in the DevCouncil trace."""
230
+ _read_stdin_payload(event_json)
231
+ root = _project_root(project_root)
232
+ try:
233
+ TraceLogger(root).log_event("session_end", {"client": client.lower()}, summary="Claude session ended.")
234
+ except Exception:
235
+ pass
236
+
237
+
238
+ @app.command()
239
+ def pre_compact(
240
+ event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
241
+ client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
242
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
243
+ ):
244
+ """Claude Code PreCompact hook: record that context compaction is about to run."""
245
+ _read_stdin_payload(event_json)
246
+ root = _project_root(project_root)
247
+ try:
248
+ TraceLogger(root).log_event("pre_compact", {"client": client.lower()}, summary="Claude context compaction starting.")
249
+ except Exception:
250
+ pass
251
+
252
+
253
+ @app.command()
254
+ def subagent_stop(
255
+ event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
256
+ client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
257
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
258
+ ):
259
+ """Claude Code SubagentStop hook: write a review signal when a subagent finishes."""
260
+ payload = _read_stdin_payload(event_json)
261
+ root = _project_root(project_root)
262
+ if not any(key in payload for key in ("task_id", "taskId", "task")):
263
+ active_id = active_task_id(root)
264
+ if active_id:
265
+ payload["task_id"] = active_id
266
+ try:
267
+ signal_path = write_signal(root, client.lower(), payload)
268
+ TraceLogger(root).log_event(
269
+ "subagent_stop",
270
+ {"client": client.lower(), "signal": str(signal_path)},
271
+ summary=f"{client} subagent finished; signal recorded.",
272
+ )
273
+ except Exception:
274
+ pass
275
+
276
+
277
+ @app.command()
278
+ def notification(
279
+ event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
280
+ client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
281
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
282
+ ):
283
+ """Claude Code Notification hook: record a Claude notification in the DevCouncil trace."""
284
+ payload = _read_stdin_payload(event_json)
285
+ root = _project_root(project_root)
286
+ try:
287
+ TraceLogger(root).log_event(
288
+ "claude_notification",
289
+ {"client": client.lower(), "message": str(payload.get("message", ""))[:200]},
290
+ summary="Claude notification.",
291
+ )
292
+ except Exception:
293
+ pass
294
+
295
+
296
+ @app.command()
297
+ def claude_statusline(
298
+ event_json: str | None = typer.Argument(None, help="The JSON status payload from Claude Code."),
299
+ project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
300
+ ):
301
+ """Claude Code statusLine command: print a compact DevCouncil status line.
302
+
303
+ Reads Claude's status JSON on stdin (cwd, model, ...) and prints one line. Falls back
304
+ to a minimal marker when the project isn't initialized so the status bar never breaks."""
305
+ payload = _read_stdin_payload(event_json)
306
+ # Prefer the cwd Claude reports so the line reflects the active workspace.
307
+ cwd = payload.get("cwd") if isinstance(payload, dict) else None
308
+ root = _project_root(Path(cwd) if isinstance(cwd, str) and cwd else project_root)
309
+ line = _status_line(root)
310
+ if not line:
311
+ print("DevCouncil: not initialized")
312
+ return
313
+ # statusLine wants a short line; drop the trailing guidance sentence.
314
+ print(line.split(". Use the")[0])
315
+
316
+
145
317
  @app.command()
146
318
  def post_task(
147
319
  client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
@@ -190,7 +190,10 @@ def initialize_project(
190
190
  dev_dir = project_root / ".devcouncil"
191
191
  created = False
192
192
 
193
- if not dev_dir.exists():
193
+ # Detect "already initialized" by the config file, NOT the .devcouncil/ directory:
194
+ # logging creates .devcouncil/logs/ on every CLI startup, so the directory exists
195
+ # before init runs. Keying on config.yaml ensures a first init still writes config.
196
+ if not (dev_dir / "config.yaml").exists():
194
197
  if not quiet:
195
198
  console.print("Initializing DevCouncil...")
196
199
  dev_dir.mkdir(exist_ok=True)
@@ -264,7 +267,9 @@ def init(
264
267
  return
265
268
 
266
269
  dev_dir = Path(".devcouncil")
267
- if dev_dir.exists() and not (with_gitnexus or with_graphify):
270
+ # "Already initialized" means a config.yaml exists not merely the .devcouncil/
271
+ # directory, which the logging setup pre-creates (.devcouncil/logs/) on startup.
272
+ if (dev_dir / "config.yaml").exists() and not (with_gitnexus or with_graphify):
268
273
  console.print("[yellow]DevCouncil is already initialized in this directory.[/yellow]")
269
274
  console.print("Use --gitnexus or --graphify to add upgrade paths.")
270
275
  raise typer.Exit()