devcouncil 0.1.1 → 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 +201 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +348 -12
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +32 -5
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +6 -3
- package/src/devcouncil/cli/commands/check.py +220 -0
- package/src/devcouncil/cli/commands/config.py +43 -4
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +6 -1
- package/src/devcouncil/cli/commands/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +379 -22
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +532 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +296 -15
- package/src/devcouncil/cli/commands/init.py +161 -20
- package/src/devcouncil/cli/commands/integrate.py +1371 -124
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +256 -55
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +50 -24
- package/src/devcouncil/cli/commands/report.py +8 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
- package/src/devcouncil/cli/commands/rollback.py +27 -28
- package/src/devcouncil/cli/commands/run.py +195 -54
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +145 -6
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/skills.py +267 -0
- package/src/devcouncil/cli/commands/status.py +30 -15
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +144 -3
- package/src/devcouncil/cli/commands/watch.py +32 -12
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +91 -7
- package/src/devcouncil/domain/evidence.py +29 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +256 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +205 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +65 -10
- package/src/devcouncil/execution/permissions.py +24 -24
- package/src/devcouncil/execution/policy_engine.py +350 -0
- package/src/devcouncil/execution/prompt_builder.py +751 -23
- package/src/devcouncil/execution/shell_session.py +231 -0
- package/src/devcouncil/execution/task_runner.py +24 -9
- package/src/devcouncil/executors/agent_registry.py +596 -0
- package/src/devcouncil/executors/coding_cli.py +791 -39
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +135 -19
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
- package/src/devcouncil/gating/policy.py +190 -11
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +17 -7
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1284 -15
- package/src/devcouncil/indexing/semantic_index.py +221 -0
- package/src/devcouncil/integrations/actions.py +166 -0
- package/src/devcouncil/integrations/check.py +426 -0
- 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 +149 -0
- package/src/devcouncil/integrations/gitnexus.py +45 -2
- package/src/devcouncil/integrations/mcp/server.py +1944 -32
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- 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 +181 -25
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +47 -11
- package/src/devcouncil/llm/cache.py +20 -8
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +617 -49
- package/src/devcouncil/llm/router.py +337 -53
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +346 -0
- package/src/devcouncil/planning/critique_service.py +16 -4
- package/src/devcouncil/planning/plan_service.py +86 -6
- package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +37 -3
- package/src/devcouncil/repo/ci_scaffold.py +165 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +384 -0
- package/src/devcouncil/reporting/json_report.py +22 -1
- package/src/devcouncil/reporting/markdown_report.py +29 -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/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +408 -0
- package/src/devcouncil/storage/db.py +140 -3
- package/src/devcouncil/storage/models.py +125 -0
- package/src/devcouncil/storage/native.py +559 -0
- package/src/devcouncil/storage/repositories.py +157 -78
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +24 -10
- package/src/devcouncil/ui/dashboard.py +393 -28
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +253 -0
- package/src/devcouncil/verification/ad_hoc_check.py +135 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +181 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1549 -143
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import asyncio
|
|
5
|
+
import logging
|
|
5
6
|
import time
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -32,6 +33,7 @@ from devcouncil.telemetry.traces import TraceLogger
|
|
|
32
33
|
|
|
33
34
|
app = typer.Typer(help="Review active coding-agent sessions and emit critique cards.")
|
|
34
35
|
console = Console()
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
@app.command("sessions")
|
|
@@ -72,6 +74,9 @@ def review(
|
|
|
72
74
|
):
|
|
73
75
|
"""Review the latest assistant response in a coding-agent transcript."""
|
|
74
76
|
root = project_root.expanduser().resolve()
|
|
77
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
78
|
+
set_log_dir(root)
|
|
79
|
+
logger.info("dev watch review: client=%s llm=%s", client, llm)
|
|
75
80
|
transcript_path = _resolve_transcript(root, client, transcript=transcript, session=session, latest=latest)
|
|
76
81
|
if transcript_path is None:
|
|
77
82
|
message = "No transcript selected. Use --transcript, --session, or --latest."
|
|
@@ -91,6 +96,7 @@ def review(
|
|
|
91
96
|
|
|
92
97
|
scoped_task_id = task_id or active_task_id(root)
|
|
93
98
|
card = asyncio.run(_review_turn(turn, root, client, llm, task_id=scoped_task_id))
|
|
99
|
+
logger.info("dev watch review: card %s verdict=%s task=%s", card.id, card.verdict, scoped_task_id or "(unscoped)")
|
|
94
100
|
saved_path, duplicate = _save_card_once(root, card, persist=persist, force=force)
|
|
95
101
|
if saved_path:
|
|
96
102
|
_log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="review")
|
|
@@ -279,18 +285,21 @@ def repair_all(
|
|
|
279
285
|
"""Generate repair prompts for all blocking live-review cards in scope."""
|
|
280
286
|
root = project_root.expanduser().resolve()
|
|
281
287
|
summary = live_review_summary(root, task_id=task_id)
|
|
288
|
+
all_cards = {card.id: card for card in load_cards(root)}
|
|
289
|
+
# Guard the membership test with isinstance(str): a malformed (non-hashable) id would
|
|
290
|
+
# otherwise raise TypeError on `in`, whereas the old per-id lookup just skipped it.
|
|
282
291
|
cards = [
|
|
283
|
-
|
|
292
|
+
all_cards[item["id"]]
|
|
284
293
|
for item in summary["blocking_cards"]
|
|
285
|
-
if isinstance(item.get("id"), str)
|
|
294
|
+
if isinstance(item.get("id"), str) and item["id"] in all_cards
|
|
286
295
|
]
|
|
287
|
-
|
|
288
|
-
prompt = build_bulk_live_repair_prompt(root,
|
|
296
|
+
resolved_cards = [card for card in cards if card is not None]
|
|
297
|
+
prompt = build_bulk_live_repair_prompt(root, resolved_cards)
|
|
289
298
|
if json_format:
|
|
290
299
|
typer.echo(json.dumps({
|
|
291
300
|
"ok": True,
|
|
292
301
|
"scope_task_id": summary["scope_task_id"],
|
|
293
|
-
"cards": [card.model_dump() for card in
|
|
302
|
+
"cards": [card.model_dump() for card in resolved_cards],
|
|
294
303
|
"prompt": prompt,
|
|
295
304
|
}, indent=2))
|
|
296
305
|
return
|
|
@@ -331,6 +340,10 @@ def pending(
|
|
|
331
340
|
):
|
|
332
341
|
"""Review every pending response-ready signal that includes a transcript path."""
|
|
333
342
|
root = project_root.expanduser().resolve()
|
|
343
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
344
|
+
set_log_dir(root)
|
|
345
|
+
logger.info("dev watch pending: reviewing signals (client=%s, llm=%s)", client or "all", llm)
|
|
346
|
+
fallback_task_id = active_task_id(root)
|
|
334
347
|
reviewed = []
|
|
335
348
|
skipped = []
|
|
336
349
|
for signal in _filtered_signals(root, client):
|
|
@@ -342,7 +355,7 @@ def pending(
|
|
|
342
355
|
if turn is None:
|
|
343
356
|
skipped.append({"signal": signal.model_dump(), "reason": f"No assistant turn found in {transcript_path}."})
|
|
344
357
|
continue
|
|
345
|
-
scoped_task_id = task_id or signal.task_id or
|
|
358
|
+
scoped_task_id = task_id or signal.task_id or fallback_task_id
|
|
346
359
|
card = asyncio.run(_review_turn(turn, root, signal.client, llm, task_id=scoped_task_id))
|
|
347
360
|
saved_path, duplicate = _save_card_once(root, card, persist=True, force=force)
|
|
348
361
|
if saved_path:
|
|
@@ -363,6 +376,7 @@ def pending(
|
|
|
363
376
|
else:
|
|
364
377
|
console.print(f"[green]Saved critique card:[/green] {saved_path}")
|
|
365
378
|
|
|
379
|
+
logger.info("dev watch pending complete: %d reviewed, %d skipped", len(reviewed), len(skipped))
|
|
366
380
|
if json_format:
|
|
367
381
|
typer.echo(json.dumps({"reviewed": reviewed, "skipped": skipped}, indent=2))
|
|
368
382
|
return
|
|
@@ -385,11 +399,15 @@ def follow(
|
|
|
385
399
|
):
|
|
386
400
|
"""Poll a transcript and emit a critique card whenever the latest assistant turn changes."""
|
|
387
401
|
root = project_root.expanduser().resolve()
|
|
402
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
403
|
+
set_log_dir(root)
|
|
388
404
|
transcript_path = _resolve_transcript(root, client, transcript=transcript, session=session, latest=latest)
|
|
389
405
|
if transcript_path is None:
|
|
406
|
+
logger.warning("dev watch follow: no transcript selected")
|
|
390
407
|
console.print("[red]No transcript selected. Use --transcript, --session, or --latest.[/red]")
|
|
391
408
|
raise typer.Exit(code=2)
|
|
392
409
|
seen_turn_id: str | None = None
|
|
410
|
+
logger.info("dev watch follow: watching %s (client=%s, interval=%ss, llm=%s)", transcript_path, client, interval, llm)
|
|
393
411
|
console.print(f"[cyan]Watching transcript:[/cyan] {transcript_path}")
|
|
394
412
|
while True:
|
|
395
413
|
turn = latest_assistant_turn(transcript_path, client=client)
|
|
@@ -397,6 +415,7 @@ def follow(
|
|
|
397
415
|
seen_turn_id = turn.turn_id
|
|
398
416
|
scoped_task_id = task_id or active_task_id(root)
|
|
399
417
|
card = asyncio.run(_review_turn(turn, root, client, llm, task_id=scoped_task_id))
|
|
418
|
+
logger.info("dev watch follow: new turn %s → card %s verdict=%s", turn.turn_id, card.id, card.verdict)
|
|
400
419
|
saved_path, duplicate = _save_card_once(root, card, persist=True, force=force)
|
|
401
420
|
if saved_path:
|
|
402
421
|
_log_card_reviewed(root, card, saved_path, duplicate=duplicate, source="follow")
|
|
@@ -557,18 +576,19 @@ def _log_signal_processed(root: Path, signal: ReviewSignal, processed_path: Path
|
|
|
557
576
|
|
|
558
577
|
async def _review_turn(turn, root: Path, client: str, use_llm: bool, task_id: str | None = None):
|
|
559
578
|
if not use_llm:
|
|
560
|
-
|
|
561
|
-
|
|
579
|
+
# Pass task_id so the deterministic review can ground completion/evidence
|
|
580
|
+
# claims against the task's real verification state.
|
|
581
|
+
return review_turn(turn, root, client=client, task_id=task_id)
|
|
562
582
|
try:
|
|
563
583
|
config = load_config(root)
|
|
564
584
|
validate_model_provider(config.models.provider)
|
|
565
585
|
api_key = get_api_key(config.models.provider, root)
|
|
566
|
-
provider = create_provider(config.models.provider, api_key)
|
|
586
|
+
provider = create_provider(config.models.provider, api_key, project_root=root, provider_prefs=config.provider)
|
|
567
587
|
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
568
|
-
router = ModelRouter(provider, role_config)
|
|
588
|
+
router = ModelRouter(provider, role_config, project_root=root)
|
|
569
589
|
except Exception as exc:
|
|
590
|
+
logger.warning("Model-backed live review unavailable; using deterministic card: %s", exc)
|
|
570
591
|
console.print(f"[yellow]Model-backed review unavailable; using deterministic card: {exc}[/yellow]")
|
|
571
|
-
|
|
572
|
-
return card.model_copy(update={"task_id": task_id}) if task_id else card
|
|
592
|
+
return review_turn(turn, root, client=client, task_id=task_id)
|
|
573
593
|
card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True)
|
|
574
594
|
return card.model_copy(update={"task_id": task_id}) if task_id else card
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import typer
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
7
|
+
from devcouncil.execution.fs_watcher import FilesystemWatcher
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def watch_fs(
|
|
13
|
+
task_id: str = typer.Option(..., "--task", help="Task ID to attribute file changes to."),
|
|
14
|
+
poll_interval: float = typer.Option(1.0, "--poll-interval"),
|
|
15
|
+
once: bool = typer.Option(False, "--once", help="Scan once and exit."),
|
|
16
|
+
project_root: Path = typer.Option(Path("."), "--project-root"),
|
|
17
|
+
json_format: bool = typer.Option(False, "--json"),
|
|
18
|
+
):
|
|
19
|
+
root = project_root.expanduser().resolve()
|
|
20
|
+
initialize_project(root, quiet=True)
|
|
21
|
+
|
|
22
|
+
def _print_event(event: dict) -> None:
|
|
23
|
+
status = "allowed" if event["allowed"] else "denied"
|
|
24
|
+
console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
|
|
25
|
+
|
|
26
|
+
watcher = FilesystemWatcher(root, task_id, poll_interval=poll_interval, on_event=_print_event)
|
|
27
|
+
if once:
|
|
28
|
+
events = watcher.scan_once()
|
|
29
|
+
if json_format:
|
|
30
|
+
typer.echo(json.dumps({"events": events}, indent=2))
|
|
31
|
+
else:
|
|
32
|
+
for event in events:
|
|
33
|
+
status = "allowed" if event["allowed"] else "denied"
|
|
34
|
+
console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
|
|
35
|
+
return
|
|
36
|
+
console.print(f"[cyan]Watching filesystem for task {task_id}. Ctrl+C to stop.[/cyan]")
|
|
37
|
+
try:
|
|
38
|
+
watcher.watch()
|
|
39
|
+
except KeyboardInterrupt:
|
|
40
|
+
console.print("[yellow]Stopped filesystem watcher.[/yellow]")
|
|
@@ -1,20 +1,53 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
1
3
|
import typer
|
|
2
|
-
from pathlib import Path
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
|
|
6
|
+
def _configure_stdio() -> None:
|
|
7
|
+
"""Make stdout/stderr resilient to non-cp1252 characters.
|
|
8
|
+
|
|
9
|
+
Coding agents and rich output emit Unicode such as ``✓``. On Windows the
|
|
10
|
+
default console / redirected-pipe encoding is cp1252, where an un-encodable
|
|
11
|
+
character raises UnicodeEncodeError mid-write. Because Rich buffers output,
|
|
12
|
+
that error can surface during an unrelated later write — which previously
|
|
13
|
+
got misreported as a coding agent "failing to start". Reconfigure both
|
|
14
|
+
streams to UTF-8 with replacement so output can never crash the process.
|
|
15
|
+
"""
|
|
16
|
+
for stream in (sys.stdout, sys.stderr):
|
|
17
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
18
|
+
if reconfigure is None:
|
|
19
|
+
continue
|
|
20
|
+
try:
|
|
21
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
22
|
+
except (ValueError, OSError):
|
|
23
|
+
try:
|
|
24
|
+
reconfigure(errors="replace")
|
|
25
|
+
except (ValueError, OSError):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_configure_stdio()
|
|
30
|
+
|
|
31
|
+
from devcouncil.cli.commands import ( # noqa: E402 - imports follow stdio reconfiguration
|
|
5
32
|
artifacts,
|
|
33
|
+
agents,
|
|
6
34
|
baseline,
|
|
35
|
+
check,
|
|
7
36
|
config,
|
|
37
|
+
cost,
|
|
8
38
|
ast,
|
|
9
39
|
dashboard,
|
|
40
|
+
design,
|
|
10
41
|
doctor,
|
|
11
42
|
go,
|
|
12
43
|
hook,
|
|
13
44
|
init,
|
|
14
45
|
integrate,
|
|
46
|
+
logs,
|
|
15
47
|
lsp,
|
|
16
48
|
map,
|
|
17
49
|
mcp_server,
|
|
50
|
+
okf,
|
|
18
51
|
plan,
|
|
19
52
|
prompt,
|
|
20
53
|
repair,
|
|
@@ -22,6 +55,7 @@ from devcouncil.cli.commands import (
|
|
|
22
55
|
reset_demo_state,
|
|
23
56
|
rollback,
|
|
24
57
|
run,
|
|
58
|
+
runs,
|
|
25
59
|
setup,
|
|
26
60
|
show,
|
|
27
61
|
status,
|
|
@@ -30,8 +64,14 @@ from devcouncil.cli.commands import (
|
|
|
30
64
|
verify,
|
|
31
65
|
version,
|
|
32
66
|
watch,
|
|
67
|
+
shell,
|
|
68
|
+
semantic,
|
|
69
|
+
evidence,
|
|
70
|
+
handoff,
|
|
71
|
+
skills,
|
|
72
|
+
scaffold,
|
|
33
73
|
)
|
|
34
|
-
from devcouncil.cli.commands.
|
|
74
|
+
from devcouncil.cli.commands.watch_fs import watch_fs # noqa: E402 - imports follow stdio reconfiguration
|
|
35
75
|
|
|
36
76
|
app = typer.Typer(
|
|
37
77
|
name="dev",
|
|
@@ -47,41 +87,85 @@ app.add_typer(report.app, name="report")
|
|
|
47
87
|
app.add_typer(rollback.app, name="rollback")
|
|
48
88
|
app.add_typer(config.app, name="config")
|
|
49
89
|
app.add_typer(artifacts.app, name="artifacts")
|
|
90
|
+
app.add_typer(agents.app, name="agents")
|
|
50
91
|
app.add_typer(hook.app, name="hook")
|
|
51
92
|
app.add_typer(version.app, name="version")
|
|
52
93
|
app.add_typer(mcp_server.app, name="mcp-server")
|
|
53
94
|
app.add_typer(integrate.app, name="integrate")
|
|
54
95
|
app.add_typer(integrate.app, name="integrations")
|
|
55
96
|
app.add_typer(trace.app, name="trace")
|
|
97
|
+
app.add_typer(logs.app, name="logs")
|
|
98
|
+
app.add_typer(cost.app, name="cost")
|
|
99
|
+
app.add_typer(runs.app, name="runs")
|
|
56
100
|
app.add_typer(setup.app, name="setup")
|
|
57
101
|
app.add_typer(lsp.app, name="lsp")
|
|
58
102
|
app.add_typer(ast.app, name="ast")
|
|
59
103
|
app.add_typer(dashboard.app, name="dashboard")
|
|
60
104
|
app.add_typer(watch.app, name="watch")
|
|
105
|
+
app.add_typer(semantic.app, name="semantic")
|
|
106
|
+
app.add_typer(evidence.app, name="evidence")
|
|
107
|
+
app.add_typer(skills.app, name="skills")
|
|
108
|
+
app.add_typer(okf.app, name="okf")
|
|
109
|
+
app.add_typer(design.app, name="design")
|
|
110
|
+
watch.app.command("fs")(watch_fs)
|
|
61
111
|
|
|
62
112
|
# Direct command registrations (those defined as def cmd())
|
|
63
113
|
app.command(name="baseline")(baseline.baseline)
|
|
64
114
|
app.command(name="e2e")(go.go)
|
|
65
115
|
app.command(name="go")(go.go)
|
|
66
116
|
app.command(name="map")(map.map_repo)
|
|
117
|
+
app.command(name="scaffold-ci")(scaffold.scaffold_ci_command)
|
|
67
118
|
app.command(name="plan")(plan.plan)
|
|
119
|
+
app.command(name="approve")(plan.approve)
|
|
68
120
|
app.command(name="prompt")(prompt.prompt)
|
|
69
121
|
app.command(name="reset-demo-state")(reset_demo_state.reset_demo_state)
|
|
70
122
|
app.command(name="run")(run.run)
|
|
123
|
+
# shell/handoff take a positional TASK_ID followed by options, so they must be
|
|
124
|
+
# plain commands — as typer sub-apps (click groups) the documented
|
|
125
|
+
# `dev shell TASK-001 --command ...` form fails to parse.
|
|
126
|
+
app.command(name="shell")(shell.shell)
|
|
127
|
+
app.command(name="handoff")(handoff.handoff)
|
|
71
128
|
app.command(name="show")(show.show)
|
|
72
129
|
app.command(name="verify")(verify.verify)
|
|
130
|
+
app.command(name="check")(check.check)
|
|
73
131
|
app.command(name="repair")(repair.repair)
|
|
74
132
|
app.command(name="status")(status.status)
|
|
133
|
+
app.command(name="optimize")(agents.optimize_agent)
|
|
75
134
|
|
|
76
135
|
@app.callback()
|
|
77
|
-
def main(
|
|
136
|
+
def main(
|
|
137
|
+
ctx: typer.Context,
|
|
138
|
+
verbose: int = typer.Option(
|
|
139
|
+
0,
|
|
140
|
+
"--verbose",
|
|
141
|
+
"-v",
|
|
142
|
+
count=True,
|
|
143
|
+
help="Increase console log verbosity (-v INFO, -vv DEBUG). Everything is "
|
|
144
|
+
"always captured at DEBUG in .devcouncil/logs/devcouncil.log.",
|
|
145
|
+
),
|
|
146
|
+
quiet: bool = typer.Option(
|
|
147
|
+
False,
|
|
148
|
+
"--quiet",
|
|
149
|
+
"-q",
|
|
150
|
+
help="Only show errors on the console (the log file still captures everything).",
|
|
151
|
+
),
|
|
152
|
+
log_level: str = typer.Option(
|
|
153
|
+
None,
|
|
154
|
+
"--log-level",
|
|
155
|
+
help="Explicit console log level (DEBUG/INFO/WARNING/ERROR). Overrides -v/-q "
|
|
156
|
+
"and the DEVCOUNCIL_LOG_LEVEL env var.",
|
|
157
|
+
),
|
|
158
|
+
):
|
|
78
159
|
"""
|
|
79
160
|
DevCouncil: Gated orchestrator for AI-assisted software development.
|
|
80
161
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
162
|
+
# Configure logging once, up front, for every command. Without this the many
|
|
163
|
+
# logger.info/debug calls across the orchestrator, planner, executors and
|
|
164
|
+
# verifier go nowhere — which is exactly why recurring run failures were so
|
|
165
|
+
# hard to diagnose. The durable DEBUG log lands in .devcouncil/logs/.
|
|
166
|
+
from devcouncil.telemetry.logging_setup import configure_logging
|
|
83
167
|
|
|
84
|
-
|
|
168
|
+
configure_logging(verbosity=verbose, quiet=quiet, log_level=log_level)
|
|
85
169
|
return
|
|
86
170
|
|
|
87
171
|
if __name__ == "__main__":
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
2
|
-
from typing import Literal, List
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
from typing import Literal, List, Dict
|
|
3
3
|
|
|
4
4
|
class CommandResult(BaseModel):
|
|
5
5
|
command: str
|
|
@@ -15,6 +15,26 @@ class DiffEvidence(BaseModel):
|
|
|
15
15
|
deleted_files: List[str]
|
|
16
16
|
diff_summary: str
|
|
17
17
|
|
|
18
|
+
class DiffCoverageEvidence(BaseModel):
|
|
19
|
+
"""Proof that the changed lines were (or were not) exercised by the tests.
|
|
20
|
+
|
|
21
|
+
This is the executable evidence behind DevCouncil's core promise: a passing
|
|
22
|
+
suite is only acceptance evidence if the lines the diff changed were actually
|
|
23
|
+
run. ``measured`` is False when no reliable signal could be computed (no
|
|
24
|
+
coverage tool, no instrumentable test command, or no changed executable
|
|
25
|
+
lines), in which case it must never be read as a defect.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
task_id: str
|
|
29
|
+
tool: str = ""
|
|
30
|
+
measured: bool = False
|
|
31
|
+
changed_lines: int = 0
|
|
32
|
+
covered_lines: int = 0
|
|
33
|
+
coverage_ratio: float = 0.0
|
|
34
|
+
uncovered_by_file: Dict[str, List[int]] = Field(default_factory=dict)
|
|
35
|
+
absent_files: List[str] = Field(default_factory=list)
|
|
36
|
+
summary: str = ""
|
|
37
|
+
|
|
18
38
|
class VerificationEvidence(BaseModel):
|
|
19
39
|
__test__ = False # Prevent pytest from collecting this as a test class
|
|
20
40
|
requirement_id: str
|
|
@@ -22,6 +42,13 @@ class VerificationEvidence(BaseModel):
|
|
|
22
42
|
command: str
|
|
23
43
|
status: Literal["passed", "failed", "not_run"]
|
|
24
44
|
evidence_summary: str
|
|
45
|
+
# HOW the criterion was proven, for auditing the gate's rigor (distinct from the
|
|
46
|
+
# pass/fail status). ``compiled`` = one DevCouncil per-criterion check passed;
|
|
47
|
+
# ``vote`` = a majority of independent checks passed (self-consistency);
|
|
48
|
+
# ``coarse`` = proven only by a passing acceptance-capable command, not a check tied
|
|
49
|
+
# to the criterion (weakest). Empty for legacy/unspecified evidence. Persisted in the
|
|
50
|
+
# evidence JSON blob, so adding it needs no migration; old rows default to "".
|
|
51
|
+
mode: Literal["compiled", "vote", "coarse", ""] = ""
|
|
25
52
|
|
|
26
53
|
# Backward-compatible alias
|
|
27
54
|
TestEvidence = VerificationEvidence
|
|
@@ -11,12 +11,17 @@ class Gap(BaseModel):
|
|
|
11
11
|
"orphan_diff",
|
|
12
12
|
"missing_test",
|
|
13
13
|
"test_failed",
|
|
14
|
+
"invalid_verification_command",
|
|
14
15
|
"acceptance_criteria_unproven",
|
|
16
|
+
"diff_not_exercised",
|
|
15
17
|
"assumption_violated",
|
|
16
18
|
"architecture_drift",
|
|
17
19
|
"security_risk",
|
|
18
20
|
"dependency_risk",
|
|
19
|
-
"migration_gap"
|
|
21
|
+
"migration_gap",
|
|
22
|
+
"quality_gate_failed",
|
|
23
|
+
"skipped_verification_command",
|
|
24
|
+
"coarse_acceptance_proof",
|
|
20
25
|
]
|
|
21
26
|
requirement_id: Optional[str] = None
|
|
22
27
|
task_id: Optional[str] = None
|
|
@@ -24,3 +29,24 @@ class Gap(BaseModel):
|
|
|
24
29
|
evidence: List[str] = Field(default_factory=list)
|
|
25
30
|
recommended_fix: str
|
|
26
31
|
blocking: bool
|
|
32
|
+
# Machine-actionable hints for the agent self-repair loop. Populated at gap
|
|
33
|
+
# creation where known; consumed by the typed next-actions contract (see
|
|
34
|
+
# devcouncil.verification.next_actions). As of schema v4 these are persisted by
|
|
35
|
+
# the gap store and round-tripped on reload, so a reconnecting agent gets the
|
|
36
|
+
# full repair contract rather than a heuristic reconstruction.
|
|
37
|
+
file: Optional[str] = None
|
|
38
|
+
line: Optional[int] = None
|
|
39
|
+
suggested_command: Optional[str] = None
|
|
40
|
+
# The acceptance criterion this gap is about (when applicable), so the agent can
|
|
41
|
+
# tie a failure straight back to the criterion it must satisfy.
|
|
42
|
+
acceptance_criterion_id: Optional[str] = None
|
|
43
|
+
# Paths to the captured stdout/stderr logs (written under .devcouncil/logs) for the
|
|
44
|
+
# failing command behind this gap, so the agent can open the FULL failure output
|
|
45
|
+
# without re-running. Optional and defaulted for backward compatibility; the gap
|
|
46
|
+
# store does not persist these, so they are only present on a fresh verify run.
|
|
47
|
+
stdout_path: Optional[str] = None
|
|
48
|
+
stderr_path: Optional[str] = None
|
|
49
|
+
# Expected verification method (e.g. "unit_test"/"static_check") for an unproven
|
|
50
|
+
# acceptance criterion, so missing-evidence routing is concrete rather than a
|
|
51
|
+
# restatement of the description.
|
|
52
|
+
expected_verification_method: Optional[str] = None
|
|
@@ -13,9 +13,38 @@ class Task(BaseModel):
|
|
|
13
13
|
requirement_ids: List[str] = Field(default_factory=list)
|
|
14
14
|
acceptance_criterion_ids: List[str] = Field(default_factory=list)
|
|
15
15
|
planned_files: List[PlannedFile] = Field(default_factory=list)
|
|
16
|
-
expected_tests: List[str] = Field(
|
|
17
|
-
|
|
16
|
+
expected_tests: List[str] = Field(
|
|
17
|
+
default_factory=list,
|
|
18
|
+
description=(
|
|
19
|
+
"Runnable shell commands (not prose) that prove this task's acceptance "
|
|
20
|
+
"criteria when they exit 0; they are executed verbatim by the verifier. "
|
|
21
|
+
"They must run right after THIS task with no missing tools or test files. "
|
|
22
|
+
"Prefer self-contained inline assertions, e.g. "
|
|
23
|
+
"python -c \"import calc; assert calc.subtract(10,3)==7\". Use pytest only "
|
|
24
|
+
"on a test file this or an earlier task creates, running the whole file "
|
|
25
|
+
"(python -m pytest tests/test_x.py -q), never a ::name node. Do not use "
|
|
26
|
+
"linters/type-checkers (flake8, mypy, ruff, eslint, tsc, npm) unless the "
|
|
27
|
+
"repo is already configured for them."
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
allowed_commands: List[str] = Field(
|
|
31
|
+
default_factory=list,
|
|
32
|
+
description=(
|
|
33
|
+
"Shell commands the executor and verifier are permitted to run for this "
|
|
34
|
+
"task (build/test/lint commands the implementation may invoke). Literal "
|
|
35
|
+
"commands only, e.g. 'python -m pytest -q', 'uv run pytest'. Leave empty "
|
|
36
|
+
"only when the task needs no commands beyond those in expected_tests."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
18
39
|
forbidden_changes: List[str] = Field(default_factory=list)
|
|
40
|
+
depends_on: List[str] = Field(
|
|
41
|
+
default_factory=list,
|
|
42
|
+
description=(
|
|
43
|
+
"IDs of tasks that must complete before this one (e.g. a task that creates a "
|
|
44
|
+
"module this task imports/tests). Used to order execution and to skip a task "
|
|
45
|
+
"whose prerequisites are unmet rather than letting it fail spuriously."
|
|
46
|
+
),
|
|
47
|
+
)
|
|
19
48
|
status: Literal[
|
|
20
49
|
"planned",
|
|
21
50
|
"ready",
|