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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""`dev logs` — find and read DevCouncil's runtime logs.
|
|
2
|
+
|
|
3
|
+
Comprehensive logging only helps if the logs are easy to reach when something
|
|
4
|
+
breaks. This command surfaces the always-on shared log
|
|
5
|
+
(``.devcouncil/logs/devcouncil.log``) and the per-run logs
|
|
6
|
+
(``.devcouncil/runs/<run_id>/run.log``) without the user needing to remember
|
|
7
|
+
paths or hand-roll ``tail``/``grep``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from devcouncil.telemetry.logging_setup import LOG_RELATIVE_PATH
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="View DevCouncil runtime logs (shared and per-run).")
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _shared_log(root: Path) -> Path:
|
|
24
|
+
return root / LOG_RELATIVE_PATH
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _runs_dir(root: Path) -> Path:
|
|
28
|
+
return root / ".devcouncil" / "runs"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _print_tail(path: Path, limit: int, grep: Optional[str]) -> None:
|
|
32
|
+
if not path.exists():
|
|
33
|
+
console.print(f"[yellow]No log at {path}. Run a command first (or pass --project-root).[/yellow]")
|
|
34
|
+
raise typer.Exit(code=0)
|
|
35
|
+
# Read only the tail rather than the whole (potentially large, rotated) file.
|
|
36
|
+
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
37
|
+
if grep:
|
|
38
|
+
lines = [line for line in lines if grep.lower() in line.lower()]
|
|
39
|
+
for line in lines[-limit:]:
|
|
40
|
+
console.print(line, markup=False, highlight=False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command("tail")
|
|
44
|
+
def tail(
|
|
45
|
+
limit: int = typer.Option(50, "--limit", "-n", help="Number of trailing lines to show."),
|
|
46
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="Keep printing new lines as they are written."),
|
|
47
|
+
grep: Optional[str] = typer.Option(None, "--grep", "-g", help="Only show lines containing this substring (case-insensitive)."),
|
|
48
|
+
run: Optional[str] = typer.Option(None, "--run", help="Show a specific run's log (.devcouncil/runs/<run>/run.log) instead of the shared log."),
|
|
49
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
50
|
+
):
|
|
51
|
+
"""Print the tail of the shared log (or a per-run log with --run)."""
|
|
52
|
+
root = project_root.expanduser().resolve()
|
|
53
|
+
path = (_runs_dir(root) / run / "run.log") if run else _shared_log(root)
|
|
54
|
+
|
|
55
|
+
_print_tail(path, limit, grep)
|
|
56
|
+
if not follow:
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Follow mode: poll for appended bytes (the file is append-only between rotations).
|
|
60
|
+
console.print(f"[dim]— following {path} (Ctrl-C to stop) —[/dim]")
|
|
61
|
+
try:
|
|
62
|
+
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
63
|
+
handle.seek(0, 2) # jump to EOF; we already printed the tail
|
|
64
|
+
while True:
|
|
65
|
+
line = handle.readline()
|
|
66
|
+
if line:
|
|
67
|
+
rendered = line.rstrip("\n")
|
|
68
|
+
if not grep or grep.lower() in rendered.lower():
|
|
69
|
+
console.print(rendered, markup=False, highlight=False)
|
|
70
|
+
else:
|
|
71
|
+
time.sleep(0.4)
|
|
72
|
+
except KeyboardInterrupt: # pragma: no cover - interactive
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("path")
|
|
77
|
+
def path(
|
|
78
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
79
|
+
):
|
|
80
|
+
"""Print the shared log file path (and whether it exists)."""
|
|
81
|
+
root = project_root.expanduser().resolve()
|
|
82
|
+
log = _shared_log(root)
|
|
83
|
+
marker = "" if log.exists() else " [yellow](not created yet)[/yellow]"
|
|
84
|
+
console.print(f"{log}{marker}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command("runs")
|
|
88
|
+
def runs(
|
|
89
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Maximum number of recent run logs to list."),
|
|
90
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
91
|
+
):
|
|
92
|
+
"""List per-run logs, newest first, with their paths."""
|
|
93
|
+
root = project_root.expanduser().resolve()
|
|
94
|
+
runs_dir = _runs_dir(root)
|
|
95
|
+
if not runs_dir.exists():
|
|
96
|
+
console.print("[yellow]No runs yet.[/yellow]")
|
|
97
|
+
return
|
|
98
|
+
run_logs = [d / "run.log" for d in runs_dir.iterdir() if (d / "run.log").exists()]
|
|
99
|
+
if not run_logs:
|
|
100
|
+
console.print("[yellow]No per-run logs yet (run.log appears once an executor runs).[/yellow]")
|
|
101
|
+
return
|
|
102
|
+
run_logs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
|
|
103
|
+
for log in run_logs[:limit]:
|
|
104
|
+
size_kb = log.stat().st_size / 1024
|
|
105
|
+
console.print(f"[bold]{log.parent.name}[/bold] [dim]{log} ({size_kb:.1f} KB)[/dim]")
|
|
106
|
+
console.print("\n[dim]View one with:[/dim] dev logs tail --run <run-id>")
|
|
@@ -5,13 +5,85 @@ import typer
|
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
|
|
7
7
|
from devcouncil.cli.commands.init import initialize_project
|
|
8
|
-
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
8
|
+
from devcouncil.indexing.repo_mapper import RepoMap, RepoMapper
|
|
9
9
|
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
10
10
|
from devcouncil.storage.db import get_db
|
|
11
11
|
|
|
12
12
|
console = Console()
|
|
13
13
|
status_console = Console(stderr=True)
|
|
14
14
|
|
|
15
|
+
AGENT_GUIDE_MARKER = "<!-- Managed by dev map: keep this file in sync with .devcouncil/repo_map.json. -->"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _important_surfaces(repo_map: RepoMap) -> list[str]:
|
|
19
|
+
"""Derive the 'important surfaces' list from the computed map, so the guide points
|
|
20
|
+
at THIS repo's real subsystems instead of hardcoded DevCouncil paths."""
|
|
21
|
+
lines: list[str] = []
|
|
22
|
+
for index, subsystem in enumerate(repo_map.subsystems[:6], start=1):
|
|
23
|
+
lines.append(f"{index}. `{subsystem.area}/` — {subsystem.summary}")
|
|
24
|
+
if not lines:
|
|
25
|
+
for index, path in enumerate(repo_map.important_files[:6], start=1):
|
|
26
|
+
lines.append(f"{index}. `{path}`")
|
|
27
|
+
return lines or ["1. See `.devcouncil/repo_map.json` for the file index."]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _agent_guide_text(repo_map_path: Path, repo_root: Path, repo_map: RepoMap) -> str:
|
|
31
|
+
return "\n".join(
|
|
32
|
+
[
|
|
33
|
+
AGENT_GUIDE_MARKER,
|
|
34
|
+
"",
|
|
35
|
+
"# Agent Workspace Guide",
|
|
36
|
+
"",
|
|
37
|
+
"Use `.devcouncil/repo_map.json` as the primary file index for this workspace.",
|
|
38
|
+
f"Repo map: `{repo_map_path.relative_to(repo_root).as_posix() if repo_map_path.is_relative_to(repo_root) else repo_map_path}`",
|
|
39
|
+
"",
|
|
40
|
+
"Workflow for agents:",
|
|
41
|
+
"1. Open `.devcouncil/repo_map.json` before guessing at file locations.",
|
|
42
|
+
"2. Use the `files` list to resolve module ownership and nearby siblings.",
|
|
43
|
+
"3. Use `subsystems` for subsystem-level navigation.",
|
|
44
|
+
"4. In `subsystems`, use `entry_points` + `critical_files` for entry points and starting context.",
|
|
45
|
+
"5. Use `role_files` in `subsystems` for subsystem role buckets (entry, runtime, policy, adapters, etc.).",
|
|
46
|
+
"6. Use `neighbors` and `handoff_paths` in `subsystems` to follow cross-subsystem flow.",
|
|
47
|
+
"7. Run `dev map` again after large refactors to refresh the map.",
|
|
48
|
+
"",
|
|
49
|
+
"Important surfaces:",
|
|
50
|
+
*_important_surfaces(repo_map),
|
|
51
|
+
"",
|
|
52
|
+
"If the map and source disagree, trust the source and regenerate the map.",
|
|
53
|
+
]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _write_agent_guides(repo_root: Path, repo_map_path: Path, repo_map: RepoMap) -> None:
|
|
58
|
+
for filename in ("AGENTS.md", "CLAUDE.md"):
|
|
59
|
+
path = repo_root / filename
|
|
60
|
+
if path.exists():
|
|
61
|
+
existing = path.read_text(encoding="utf-8")
|
|
62
|
+
if AGENT_GUIDE_MARKER not in existing:
|
|
63
|
+
continue
|
|
64
|
+
path.write_text(_agent_guide_text(repo_map_path, repo_root, repo_map) + "\n", encoding="utf-8")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def generate_map_artifacts(root: Path, output: Path, goal: str = "", *, scan_dependencies: bool = False) -> RepoMap:
|
|
68
|
+
"""Build the repo map and write repo_map.json + agent guides (no LLM, no re-init).
|
|
69
|
+
|
|
70
|
+
Assumes ``.devcouncil/`` already exists. Shared by the ``dev map`` command and
|
|
71
|
+
by project initialization so a freshly set-up repo is immediately navigable.
|
|
72
|
+
``scan_dependencies`` is opt-in (off for init and default mapping) because it can
|
|
73
|
+
shell out to dependency auditors.
|
|
74
|
+
"""
|
|
75
|
+
repo_map = RepoMapper(root).map_repo(goal, scan_dependencies=scan_dependencies)
|
|
76
|
+
graph_context = CodeReviewGraphAdapter(root).get_context()
|
|
77
|
+
output = output if output.is_absolute() else root / output
|
|
78
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
output.write_text(repo_map.model_dump_json(indent=2), encoding="utf-8")
|
|
80
|
+
_write_agent_guides(root, output, repo_map)
|
|
81
|
+
if graph_context.available:
|
|
82
|
+
graph_output = output.with_name("code_review_graph_context.json")
|
|
83
|
+
graph_output.write_text(graph_context.model_dump_json(indent=2), encoding="utf-8")
|
|
84
|
+
status_console.print(f"[green]Wrote code-review-graph context to {graph_output}[/green]")
|
|
85
|
+
return repo_map
|
|
86
|
+
|
|
15
87
|
|
|
16
88
|
def map_repo(
|
|
17
89
|
goal: str = typer.Argument("", help="Goal text used for candidate-file ranking."),
|
|
@@ -22,21 +94,19 @@ def map_repo(
|
|
|
22
94
|
help="Path to write repo_map.json.",
|
|
23
95
|
),
|
|
24
96
|
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
97
|
+
scan_deps: bool = typer.Option(
|
|
98
|
+
False,
|
|
99
|
+
"--scan-deps",
|
|
100
|
+
help="Run available dependency auditors (pip-audit/npm audit/osv-scanner) and record dependency_risks in the map. Off by default.",
|
|
101
|
+
),
|
|
25
102
|
):
|
|
26
103
|
"""Build the deterministic repository map without calling an LLM."""
|
|
27
104
|
root = project_root.expanduser().resolve()
|
|
28
|
-
initialize_project(root, quiet=True)
|
|
105
|
+
initialize_project(root, quiet=True, with_map=False)
|
|
29
106
|
if not get_db(root):
|
|
30
107
|
raise typer.Exit(code=1)
|
|
31
108
|
|
|
32
|
-
repo_map = RepoMapper(root).map_repo(goal)
|
|
33
|
-
graph_context = CodeReviewGraphAdapter(root).get_context()
|
|
34
109
|
output = output if output.is_absolute() else root / output
|
|
35
|
-
|
|
36
|
-
output.write_text(repo_map.model_dump_json(indent=2), encoding="utf-8")
|
|
37
|
-
if graph_context.available:
|
|
38
|
-
graph_output = output.with_name("code_review_graph_context.json")
|
|
39
|
-
graph_output.write_text(graph_context.model_dump_json(indent=2), encoding="utf-8")
|
|
40
|
-
status_console.print(f"[green]Wrote code-review-graph context to {graph_output}[/green]")
|
|
110
|
+
repo_map = generate_map_artifacts(root, output, goal, scan_dependencies=scan_deps)
|
|
41
111
|
typer.echo(json.dumps(repo_map.model_dump(), indent=2))
|
|
42
112
|
status_console.print(f"[green]Wrote repository map to {output}[/green]")
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""`dev okf` — export, ingest, and validate Open Knowledge Format bundles.
|
|
2
|
+
|
|
3
|
+
* ``dev okf export`` renders DevCouncil's artifact graph as a portable OKF bundle.
|
|
4
|
+
* ``dev okf ingest`` imports an external OKF bundle as planning/coding context under
|
|
5
|
+
``.devcouncil/knowledge/okf/`` (selected into prompts like a domain skill).
|
|
6
|
+
* ``dev okf validate`` checks a bundle for the OKF invariants (typed docs, resolved links).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
19
|
+
from devcouncil.knowledge.okf import read_bundle, validate_bundle
|
|
20
|
+
from devcouncil.storage.db import get_db
|
|
21
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(help="Export, ingest, and validate Open Knowledge Format bundles.")
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _knowledge_okf_dir(root: Path) -> Path:
|
|
28
|
+
from devcouncil.app.config import load_config
|
|
29
|
+
|
|
30
|
+
directory = ".devcouncil/knowledge"
|
|
31
|
+
try:
|
|
32
|
+
directory = load_config(root).knowledge.directory
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return root / directory / "okf"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _knowledge_design_md(root: Path) -> Path | None:
|
|
39
|
+
"""Locate the project's design.md under the configured knowledge dir's design/ subdir.
|
|
40
|
+
|
|
41
|
+
Honors the same config dir resolution as :func:`_knowledge_okf_dir`, falling back to the
|
|
42
|
+
default ``.devcouncil/knowledge/design/design.md``. Returns ``None`` if no design.md exists.
|
|
43
|
+
"""
|
|
44
|
+
from devcouncil.app.config import load_config
|
|
45
|
+
|
|
46
|
+
directory = ".devcouncil/knowledge"
|
|
47
|
+
try:
|
|
48
|
+
directory = load_config(root).knowledge.directory
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
candidates = [
|
|
52
|
+
root / directory / "design" / "design.md",
|
|
53
|
+
root / ".devcouncil" / "knowledge" / "design" / "design.md",
|
|
54
|
+
]
|
|
55
|
+
for candidate in candidates:
|
|
56
|
+
if candidate.is_file():
|
|
57
|
+
return candidate
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.command("export")
|
|
62
|
+
def export(
|
|
63
|
+
output: Path = typer.Option(Path("okf_bundle"), "--output", "-o", help="Directory to write the OKF bundle into."),
|
|
64
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
65
|
+
skills: bool = typer.Option(
|
|
66
|
+
True,
|
|
67
|
+
"--skills/--no-skills",
|
|
68
|
+
help="Include the engineering skills library as OKF documents in the bundle.",
|
|
69
|
+
),
|
|
70
|
+
design: bool = typer.Option(
|
|
71
|
+
True,
|
|
72
|
+
"--design/--no-design",
|
|
73
|
+
help="Include the project's design.md (if present) as an OKF document in the bundle.",
|
|
74
|
+
),
|
|
75
|
+
):
|
|
76
|
+
"""Export the DevCouncil artifact graph as an OKF bundle."""
|
|
77
|
+
root = project_root.expanduser().resolve()
|
|
78
|
+
initialize_project(root, quiet=True)
|
|
79
|
+
db = get_db(root)
|
|
80
|
+
if not db:
|
|
81
|
+
console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
|
|
82
|
+
raise typer.Exit(code=1)
|
|
83
|
+
|
|
84
|
+
out_dir = output.expanduser().resolve()
|
|
85
|
+
with db.get_session() as session:
|
|
86
|
+
graph = ArtifactGraphRepository(session).load_graph()
|
|
87
|
+
|
|
88
|
+
project_name = root.name or "DevCouncil Project"
|
|
89
|
+
try:
|
|
90
|
+
from devcouncil.app.config import load_config
|
|
91
|
+
|
|
92
|
+
project_name = load_config(root).project.name or project_name
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Load the FULL skill set (packaged library + this repo's own skills) so the export is
|
|
97
|
+
# complete; goal-driven selection would only emit the few skills that match a goal.
|
|
98
|
+
skill_list: list = []
|
|
99
|
+
if skills:
|
|
100
|
+
from devcouncil.skills.registry import load_skills
|
|
101
|
+
|
|
102
|
+
skill_list = load_skills(project_root=root)
|
|
103
|
+
|
|
104
|
+
# Look for the project's design.md under the configured knowledge dir; silently skip if
|
|
105
|
+
# absent so export stays useful for projects without a design system.
|
|
106
|
+
design_obj = None
|
|
107
|
+
if design:
|
|
108
|
+
design_md = _knowledge_design_md(root)
|
|
109
|
+
if design_md is not None:
|
|
110
|
+
from devcouncil.knowledge.design import parse_design_md
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
design_obj = parse_design_md(design_md)
|
|
114
|
+
except Exception:
|
|
115
|
+
design_obj = None
|
|
116
|
+
|
|
117
|
+
timestamp = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
118
|
+
from devcouncil.reporting.okf_bundle_writer import OKFBundleWriter
|
|
119
|
+
|
|
120
|
+
written = OKFBundleWriter.generate(
|
|
121
|
+
graph,
|
|
122
|
+
out_dir,
|
|
123
|
+
project_name=project_name,
|
|
124
|
+
timestamp=timestamp,
|
|
125
|
+
include_skills=bool(skill_list),
|
|
126
|
+
skills=skill_list,
|
|
127
|
+
include_design=design_obj is not None,
|
|
128
|
+
design=design_obj,
|
|
129
|
+
)
|
|
130
|
+
console.print(
|
|
131
|
+
f"[green]Exported {len(written)} OKF documents to[/green] {out_dir} "
|
|
132
|
+
f"([cyan]{out_dir / 'index.md'}[/cyan])."
|
|
133
|
+
)
|
|
134
|
+
if skill_list:
|
|
135
|
+
console.print(f"[green]Included {len(skill_list)} engineering skill document(s).[/green]")
|
|
136
|
+
if design_obj is not None:
|
|
137
|
+
console.print("[green]Included 1 design system document(s).[/green]")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("ingest")
|
|
141
|
+
def ingest(
|
|
142
|
+
bundle: str = typer.Argument(
|
|
143
|
+
...,
|
|
144
|
+
help="OKF bundle source: a local directory, a .tar.gz/.tgz/.zip archive, or a git URL.",
|
|
145
|
+
),
|
|
146
|
+
name: str = typer.Option("", "--name", help="Subfolder name under knowledge/okf (defaults to the bundle name)."),
|
|
147
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
148
|
+
):
|
|
149
|
+
"""Ingest an external OKF bundle as durable planning/coding context.
|
|
150
|
+
|
|
151
|
+
The bundle may be a local directory, a local archive (``.tar.gz``/``.tgz``/``.zip``,
|
|
152
|
+
extracted behind a path-traversal guard), or a git URL (shallow-cloned). After the
|
|
153
|
+
source is materialized to a local directory, the existing read/validate/copy logic runs
|
|
154
|
+
unchanged; any temp directory is removed afterwards.
|
|
155
|
+
"""
|
|
156
|
+
root = project_root.expanduser().resolve()
|
|
157
|
+
|
|
158
|
+
from devcouncil.knowledge.fetch import fetch_bundle
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
fetched = fetch_bundle(bundle)
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
console.print(f"[red]Could not fetch bundle[/red] {bundle!r}: {exc}")
|
|
164
|
+
raise typer.Exit(code=1)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
src = fetched.directory
|
|
168
|
+
if not src.is_dir():
|
|
169
|
+
console.print(f"[red]Not a directory:[/red] {src}")
|
|
170
|
+
raise typer.Exit(code=1)
|
|
171
|
+
|
|
172
|
+
initialize_project(root, quiet=True)
|
|
173
|
+
parsed = read_bundle(src)
|
|
174
|
+
if not parsed.documents:
|
|
175
|
+
console.print(f"[yellow]No OKF documents (*.md) found in[/yellow] {bundle}")
|
|
176
|
+
raise typer.Exit(code=1)
|
|
177
|
+
|
|
178
|
+
problems = validate_bundle(parsed)
|
|
179
|
+
if problems:
|
|
180
|
+
console.print(f"[yellow]Ingesting a bundle with {len(problems)} validation issue(s):[/yellow]")
|
|
181
|
+
for p in problems[:10]:
|
|
182
|
+
console.print(f" - {p}")
|
|
183
|
+
|
|
184
|
+
dest = _knowledge_okf_dir(root) / (name or fetched.suggested_name or "bundle")
|
|
185
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
186
|
+
count = 0
|
|
187
|
+
for md in src.rglob("*.md"):
|
|
188
|
+
rel = md.relative_to(src)
|
|
189
|
+
target = dest / rel
|
|
190
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
shutil.copyfile(md, target)
|
|
192
|
+
count += 1
|
|
193
|
+
console.print(
|
|
194
|
+
f"[green]Ingested {count} OKF document(s) into[/green] {dest}. "
|
|
195
|
+
"They are now available as planning/coding context."
|
|
196
|
+
)
|
|
197
|
+
finally:
|
|
198
|
+
fetched.cleanup()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@app.command("validate")
|
|
202
|
+
def validate(
|
|
203
|
+
bundle: Path = typer.Argument(..., help="Path to an OKF bundle directory to validate."),
|
|
204
|
+
):
|
|
205
|
+
"""Validate an OKF bundle (every doc typed; every intra-bundle link resolves)."""
|
|
206
|
+
src = bundle.expanduser().resolve()
|
|
207
|
+
if not src.is_dir():
|
|
208
|
+
console.print(f"[red]Not a directory:[/red] {src}")
|
|
209
|
+
raise typer.Exit(code=1)
|
|
210
|
+
|
|
211
|
+
parsed = read_bundle(src)
|
|
212
|
+
problems = validate_bundle(parsed)
|
|
213
|
+
if not problems:
|
|
214
|
+
console.print(f"[green]✓ Valid OKF bundle[/green] — {len(parsed.documents)} document(s), all links resolve.")
|
|
215
|
+
return
|
|
216
|
+
console.print(f"[red]✗ {len(problems)} problem(s) in[/red] {src}:")
|
|
217
|
+
for p in problems:
|
|
218
|
+
console.print(f" - {p}")
|
|
219
|
+
raise typer.Exit(code=1)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.command("html")
|
|
223
|
+
def html(
|
|
224
|
+
bundle: Path = typer.Argument(..., help="Path to an OKF bundle directory to render."),
|
|
225
|
+
output: Path = typer.Option(Path("okf_site"), "--output", "-o", help="Directory to write the static HTML site into."),
|
|
226
|
+
):
|
|
227
|
+
"""Render an OKF bundle as a browsable, self-contained static HTML site."""
|
|
228
|
+
src = bundle.expanduser().resolve()
|
|
229
|
+
if not src.is_dir():
|
|
230
|
+
console.print(f"[red]Not a directory:[/red] {src}")
|
|
231
|
+
raise typer.Exit(code=1)
|
|
232
|
+
|
|
233
|
+
parsed = read_bundle(src)
|
|
234
|
+
if not parsed.documents:
|
|
235
|
+
console.print(f"[yellow]No OKF documents (*.md) found in[/yellow] {src}")
|
|
236
|
+
raise typer.Exit(code=1)
|
|
237
|
+
|
|
238
|
+
from devcouncil.reporting.okf_html import write_bundle_html
|
|
239
|
+
|
|
240
|
+
out_dir = output.expanduser().resolve()
|
|
241
|
+
written = write_bundle_html(parsed, out_dir)
|
|
242
|
+
console.print(
|
|
243
|
+
f"[green]Rendered {len(written)} page(s) to[/green] {out_dir} "
|
|
244
|
+
f"([cyan]{out_dir / 'index.html'}[/cyan])."
|
|
245
|
+
)
|