devcouncil 0.2.0 → 0.3.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.
- package/README.md +12 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/devcouncil/app/config.py +181 -7
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +9 -2
- package/src/devcouncil/cli/commands/check.py +12 -1
- package/src/devcouncil/cli/commands/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +160 -3
- package/src/devcouncil/cli/commands/go.py +96 -16
- package/src/devcouncil/cli/commands/hook.py +172 -0
- package/src/devcouncil/cli/commands/init.py +7 -2
- package/src/devcouncil/cli/commands/integrate.py +492 -34
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +54 -14
- package/src/devcouncil/cli/commands/repair.py +12 -3
- package/src/devcouncil/cli/commands/run.py +128 -7
- package/src/devcouncil/cli/commands/skills.py +180 -1
- package/src/devcouncil/cli/commands/status.py +7 -16
- package/src/devcouncil/cli/commands/verify.py +16 -10
- package/src/devcouncil/cli/commands/watch.py +24 -4
- package/src/devcouncil/cli/main.py +36 -1
- package/src/devcouncil/domain/evidence.py +7 -0
- package/src/devcouncil/execution/checkpoints.py +12 -2
- package/src/devcouncil/execution/fs_watcher.py +27 -2
- package/src/devcouncil/execution/handoff.py +1 -1
- package/src/devcouncil/execution/patch.py +6 -0
- package/src/devcouncil/execution/permissions.py +7 -0
- package/src/devcouncil/execution/policy_engine.py +12 -5
- package/src/devcouncil/execution/prompt_builder.py +126 -10
- package/src/devcouncil/execution/shell_session.py +6 -0
- package/src/devcouncil/execution/task_runner.py +18 -7
- package/src/devcouncil/executors/agent_registry.py +22 -1
- package/src/devcouncil/executors/coding_cli.py +133 -5
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +15 -0
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/secret_scan_check.py +7 -0
- package/src/devcouncil/gating/policy.py +38 -7
- package/src/devcouncil/indexing/ast_matcher.py +16 -6
- package/src/devcouncil/indexing/repo_mapper.py +30 -8
- package/src/devcouncil/indexing/semantic_index.py +42 -26
- package/src/devcouncil/integrations/actions.py +24 -4
- package/src/devcouncil/integrations/check.py +7 -4
- package/src/devcouncil/integrations/claude_assets.py +444 -0
- package/src/devcouncil/integrations/code_review_graph.py +13 -2
- package/src/devcouncil/integrations/github_intent.py +8 -1
- package/src/devcouncil/integrations/gitnexus.py +10 -2
- package/src/devcouncil/integrations/mcp/server.py +404 -15
- package/src/devcouncil/integrations/pr_comments.py +9 -0
- package/src/devcouncil/knowledge/__init__.py +23 -0
- package/src/devcouncil/knowledge/design.py +374 -0
- package/src/devcouncil/knowledge/design_conformance.py +317 -0
- package/src/devcouncil/knowledge/fetch.py +223 -0
- package/src/devcouncil/knowledge/frontmatter.py +51 -0
- package/src/devcouncil/knowledge/okf.py +202 -0
- package/src/devcouncil/knowledge/skill_bridge.py +96 -0
- package/src/devcouncil/knowledge/sources.py +239 -0
- package/src/devcouncil/live/cards.py +20 -6
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +38 -5
- package/src/devcouncil/llm/cache.py +14 -6
- package/src/devcouncil/llm/provider.py +179 -92
- package/src/devcouncil/llm/router.py +122 -23
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +47 -4
- package/src/devcouncil/planning/critique_service.py +9 -2
- package/src/devcouncil/planning/plan_service.py +69 -3
- package/src/devcouncil/planning/prompt_enhancer_service.py +124 -0
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +10 -2
- package/src/devcouncil/repo/ci_scaffold.py +13 -5
- package/src/devcouncil/repo/sca.py +11 -1
- package/src/devcouncil/reporting/json_report.py +11 -0
- package/src/devcouncil/reporting/markdown_report.py +14 -1
- package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
- package/src/devcouncil/reporting/okf_html.py +323 -0
- package/src/devcouncil/reporting/report_builder.py +18 -1
- package/src/devcouncil/skills/registry.py +111 -33
- package/src/devcouncil/storage/db.py +58 -2
- package/src/devcouncil/storage/models.py +4 -0
- package/src/devcouncil/storage/native.py +20 -18
- package/src/devcouncil/storage/repositories.py +35 -18
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/tracker.py +12 -1
- package/src/devcouncil/ui/dashboard.py +69 -5
- package/src/devcouncil/verification/acceptance_compiler.py +147 -19
- package/src/devcouncil/verification/ad_hoc_check.py +6 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/sandbox.py +7 -4
- package/src/devcouncil/verification/verifier.py +905 -517
- 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
|
-
|
|
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
|
-
|
|
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()
|