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.
- 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
|
@@ -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>")
|
|
@@ -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
|
+
)
|
|
@@ -2,6 +2,7 @@ import typer
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import json
|
|
4
4
|
import datetime
|
|
5
|
+
import logging
|
|
5
6
|
from typing import Any
|
|
6
7
|
from rich.console import Console
|
|
7
8
|
from rich.panel import Panel
|
|
@@ -18,8 +19,8 @@ from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
|
18
19
|
from devcouncil.llm.provider import Provider, MockProvider, ProviderRequestError, build_role_model_config, create_provider, validate_model_provider
|
|
19
20
|
from devcouncil.llm.router import ModelRouter, StructuredOutputError
|
|
20
21
|
from devcouncil.planning.spec_service import SpecService
|
|
21
|
-
from devcouncil.planning.prompt_enhancer_service import PromptEnhancerService
|
|
22
|
-
from devcouncil.planning.plan_service import PlanService
|
|
22
|
+
from devcouncil.planning.prompt_enhancer_service import PromptEnhancerService, save_active_prompt_enhancement
|
|
23
|
+
from devcouncil.planning.plan_service import PlanService, backfill_acceptance_criteria
|
|
23
24
|
from devcouncil.planning.critique_service import CritiqueService
|
|
24
25
|
from devcouncil.planning.arbiter_service import ArbiterDecision, ArbiterService
|
|
25
26
|
from devcouncil.gating.policy import GatePolicy
|
|
@@ -28,9 +29,11 @@ from devcouncil.app.state_machine import ProjectPhase
|
|
|
28
29
|
from devcouncil.app.config import ModelRoleConfig, load_config, get_api_key
|
|
29
30
|
from devcouncil.cli.commands.init import initialize_project
|
|
30
31
|
from devcouncil.telemetry.traces import TraceLogger
|
|
32
|
+
from devcouncil.telemetry.stages import log_step
|
|
31
33
|
|
|
32
34
|
app = typer.Typer()
|
|
33
35
|
console = Console()
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
34
37
|
|
|
35
38
|
REQUIRED_PLANNING_ROLES = (
|
|
36
39
|
"prompt_enhancer",
|
|
@@ -92,6 +95,8 @@ async def run_plan_flow(
|
|
|
92
95
|
quick: bool = False,
|
|
93
96
|
):
|
|
94
97
|
root = project_root.expanduser().resolve()
|
|
98
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
99
|
+
set_log_dir(root)
|
|
95
100
|
initialize_project(root, quiet=True)
|
|
96
101
|
db = get_db(root)
|
|
97
102
|
if not db:
|
|
@@ -166,7 +171,7 @@ async def run_plan_flow(
|
|
|
166
171
|
if api_key is None:
|
|
167
172
|
console.print("[red]Missing API key for configured model provider.[/red]")
|
|
168
173
|
return []
|
|
169
|
-
provider = create_provider(config.models.provider, api_key, project_root=root)
|
|
174
|
+
provider = create_provider(config.models.provider, api_key, project_root=root, provider_prefs=config.provider)
|
|
170
175
|
|
|
171
176
|
# Build role config after dry-run overrides so mocks are routed correctly.
|
|
172
177
|
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
@@ -185,16 +190,20 @@ async def run_plan_flow(
|
|
|
185
190
|
transient=True,
|
|
186
191
|
) as progress:
|
|
187
192
|
# 1. Repo Map
|
|
193
|
+
log_step("plan/1: mapping repository", project_root=root, run_id=run_id)
|
|
188
194
|
progress.add_task(description="Mapping repository...", total=None)
|
|
189
195
|
repo_map = mapper.map_repo(goal)
|
|
190
196
|
repo_map_json = repo_map.model_dump_json(indent=2)
|
|
191
|
-
|
|
197
|
+
# save_run_artifact re-serializes via json.dump, so pass the dict directly
|
|
198
|
+
# instead of round-tripping the already-serialized JSON back through json.loads.
|
|
199
|
+
orchestrator.save_run_artifact("repo_map.json", repo_map.model_dump(mode="json"))
|
|
192
200
|
graph_context = CodeReviewGraphAdapter(root).get_context()
|
|
193
201
|
if graph_context.available:
|
|
194
202
|
orchestrator.save_run_artifact("code_review_graph_context.json", graph_context.model_dump())
|
|
195
203
|
await orchestrator.transition_to(ProjectPhase.REPO_MAPPED)
|
|
196
204
|
|
|
197
205
|
# 2. Codebase-specific prompt enhancement
|
|
206
|
+
log_step("plan/2: enhancing prompt for codebase debate", project_root=root, run_id=run_id)
|
|
198
207
|
progress.add_task(description="Enhancing prompt for codebase debate...", total=None)
|
|
199
208
|
prompt_enhancement = await prompt_enhancer.enhance_prompt(
|
|
200
209
|
goal,
|
|
@@ -225,6 +234,7 @@ async def run_plan_flow(
|
|
|
225
234
|
)
|
|
226
235
|
|
|
227
236
|
# 3. Spec / Requirements
|
|
237
|
+
log_step("plan/3: generating requirements", project_root=root, run_id=run_id)
|
|
228
238
|
progress.add_task(description="Generating requirements...", total=None)
|
|
229
239
|
spec_output = await spec_service.generate_spec(debate_goal, repo_map_json)
|
|
230
240
|
orchestrator.save_run_artifact("requirements.json", spec_output.model_dump())
|
|
@@ -243,6 +253,7 @@ async def run_plan_flow(
|
|
|
243
253
|
# adversarial robustness for ~5 fewer model calls — the right setting
|
|
244
254
|
# for small, well-scoped changes where verification (which still gates
|
|
245
255
|
# every diff) is the real safety net, not planning debate.
|
|
256
|
+
log_step("plan/4: generating single plan (quick mode)", project_root=root, run_id=run_id)
|
|
246
257
|
progress.add_task(description="Generating single plan (quick mode)...", total=None)
|
|
247
258
|
plan_a = await plan_service.generate_plan(
|
|
248
259
|
"planner_a", debate_goal, requirements_json, repo_map_json
|
|
@@ -266,6 +277,7 @@ async def run_plan_flow(
|
|
|
266
277
|
final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
|
|
267
278
|
else:
|
|
268
279
|
# 4. Independent Plans (run concurrently — they don't depend on each other)
|
|
280
|
+
log_step("plan/4: generating Plans A (pragmatic) and B (robust)", project_root=root, run_id=run_id)
|
|
269
281
|
progress.add_task(description="Generating Plans A (Pragmatic) and B (Robust)...", total=None)
|
|
270
282
|
plan_a, plan_b = await asyncio.gather(
|
|
271
283
|
plan_service.generate_plan("planner_a", debate_goal, requirements_json, repo_map_json),
|
|
@@ -275,34 +287,44 @@ async def run_plan_flow(
|
|
|
275
287
|
orchestrator.save_run_artifact("plan_b.json", plan_b.model_dump())
|
|
276
288
|
await orchestrator.transition_to(ProjectPhase.PLANS_GENERATED)
|
|
277
289
|
|
|
290
|
+
# Serialize each plan/critique once and reuse the strings across the
|
|
291
|
+
# critique, rebuttal, and arbitration steps below.
|
|
292
|
+
plan_a_json = plan_a.model_dump_json()
|
|
293
|
+
plan_b_json = plan_b.model_dump_json()
|
|
294
|
+
|
|
278
295
|
# 5. Cross-Critique (independent — run concurrently)
|
|
296
|
+
log_step("plan/5: cross-critiquing Plans A and B", project_root=root, run_id=run_id)
|
|
279
297
|
progress.add_task(description="Critiquing Plans A and B...", total=None)
|
|
280
298
|
critique_a, critique_b = await asyncio.gather(
|
|
281
|
-
critique_service.generate_critique("critic_a",
|
|
282
|
-
critique_service.generate_critique("critic_b",
|
|
299
|
+
critique_service.generate_critique("critic_a", plan_b_json, requirements_json),
|
|
300
|
+
critique_service.generate_critique("critic_b", plan_a_json, requirements_json),
|
|
283
301
|
)
|
|
284
302
|
orchestrator.save_run_artifact("critique_a.json", critique_a.model_dump())
|
|
285
303
|
orchestrator.save_run_artifact("critique_b.json", critique_b.model_dump())
|
|
286
304
|
await orchestrator.transition_to(ProjectPhase.CRITIQUES_GENERATED)
|
|
305
|
+
critique_a_json = critique_a.model_dump_json()
|
|
306
|
+
critique_b_json = critique_b.model_dump_json()
|
|
287
307
|
|
|
288
308
|
# 6. Rebuttals (independent — run concurrently)
|
|
309
|
+
log_step("plan/6: generating rebuttals", project_root=root, run_id=run_id)
|
|
289
310
|
progress.add_task(description="Generating rebuttals...", total=None)
|
|
290
311
|
rebuttal_a, rebuttal_b = await asyncio.gather(
|
|
291
|
-
critique_service.generate_rebuttal("planner_a",
|
|
292
|
-
critique_service.generate_rebuttal("planner_b",
|
|
312
|
+
critique_service.generate_rebuttal("planner_a", plan_a_json, critique_b_json),
|
|
313
|
+
critique_service.generate_rebuttal("planner_b", plan_b_json, critique_a_json),
|
|
293
314
|
)
|
|
294
315
|
orchestrator.save_run_artifact("rebuttal_a.json", rebuttal_a.model_dump())
|
|
295
316
|
orchestrator.save_run_artifact("rebuttal_b.json", rebuttal_b.model_dump())
|
|
296
317
|
|
|
297
318
|
# 7. Arbitration
|
|
319
|
+
log_step("plan/7: arbitrating final plan", project_root=root, run_id=run_id)
|
|
298
320
|
progress.add_task(description="Arbitrating final plan...", total=None)
|
|
299
321
|
decision = await arbiter_service.arbitrate(
|
|
300
322
|
debate_goal,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
323
|
+
requirements_json,
|
|
324
|
+
plan_a_json,
|
|
325
|
+
plan_b_json,
|
|
326
|
+
critique_a_json,
|
|
327
|
+
critique_b_json,
|
|
306
328
|
rebuttal_a.model_dump_json(),
|
|
307
329
|
rebuttal_b.model_dump_json()
|
|
308
330
|
)
|
|
@@ -317,10 +339,23 @@ async def run_plan_flow(
|
|
|
317
339
|
console.print("[blue](DRY RUN: No actual LLM calls were made)[/blue]")
|
|
318
340
|
if not persist:
|
|
319
341
|
console.print("[blue](DRY RUN: Final requirements/tasks were not persisted)[/blue]")
|
|
342
|
+
# Operationalize the spec's edge-case elaboration: attach every acceptance criterion
|
|
343
|
+
# the planner left unlinked to a task that owns its requirement, so elaborated edges
|
|
344
|
+
# (truncation semantics, error paths, boundaries) are actually built and per-criterion
|
|
345
|
+
# verified instead of silently dropped — the gap that lets a gated run be no better
|
|
346
|
+
# than the raw prompt.
|
|
347
|
+
final_tasks, backfilled_acs = backfill_acceptance_criteria(final_tasks, decision.final_requirements)
|
|
348
|
+
if backfilled_acs:
|
|
349
|
+
console.print(
|
|
350
|
+
f"[dim]Linked {len(backfilled_acs)} unmapped acceptance criterion(s) to owning task(s) "
|
|
351
|
+
"so every elaborated behavior is verified.[/dim]"
|
|
352
|
+
)
|
|
353
|
+
|
|
320
354
|
console.print(f"Final Requirements: [bold]{len(decision.final_requirements)}[/bold]")
|
|
321
355
|
console.print(f"Final Tasks: [bold]{len(final_tasks)}[/bold]")
|
|
322
|
-
|
|
356
|
+
|
|
323
357
|
# 8. Check Gates
|
|
358
|
+
log_step("plan/8: checking plan-approval gates", project_root=root, run_id=run_id)
|
|
324
359
|
policy = GatePolicy()
|
|
325
360
|
result = policy.check_plan_approval(
|
|
326
361
|
decision.final_requirements,
|
|
@@ -342,8 +377,12 @@ async def run_plan_flow(
|
|
|
342
377
|
final_tasks,
|
|
343
378
|
reconciled_findings,
|
|
344
379
|
)
|
|
380
|
+
# Pin THIS plan's enhancement so the executor reads the domain guidance tied to
|
|
381
|
+
# the plan it runs (not a later run's by mtime).
|
|
382
|
+
save_active_prompt_enhancement(root, prompt_enhancement)
|
|
345
383
|
|
|
346
384
|
console.print("[green]Plan approved by gates.[/green]")
|
|
385
|
+
logger.info("Plan approved by gates: %d task(s)", len(final_tasks))
|
|
347
386
|
await orchestrator.transition_to(ProjectPhase.PLAN_APPROVED)
|
|
348
387
|
return [task.id for task in final_tasks]
|
|
349
388
|
else:
|
|
@@ -353,6 +392,7 @@ async def run_plan_flow(
|
|
|
353
392
|
for gap in result.gaps:
|
|
354
393
|
gap_repo.save(gap)
|
|
355
394
|
console.print("[yellow]Plan generated but failed gates. See status for gaps.[/yellow]")
|
|
395
|
+
logger.warning("Plan failed approval gates with %d gap(s)", len(result.gaps))
|
|
356
396
|
await orchestrator.transition_to(ProjectPhase.AWAITING_USER_DECISIONS)
|
|
357
397
|
return []
|
|
358
398
|
|
|
@@ -9,13 +9,17 @@ from devcouncil.llm.router import ModelRouter
|
|
|
9
9
|
from devcouncil.app.config import load_config, get_api_key
|
|
10
10
|
from devcouncil.cli.commands.init import initialize_project
|
|
11
11
|
import asyncio
|
|
12
|
+
import logging
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
14
15
|
app = typer.Typer()
|
|
15
16
|
console = Console()
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
17
19
|
async def run_repair_flow(project_root: Path = Path(".")):
|
|
18
20
|
root = project_root.expanduser().resolve()
|
|
21
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
22
|
+
set_log_dir(root)
|
|
19
23
|
initialize_project(root, quiet=True)
|
|
20
24
|
db = get_db(root)
|
|
21
25
|
if not db:
|
|
@@ -28,25 +32,29 @@ async def run_repair_flow(project_root: Path = Path(".")):
|
|
|
28
32
|
|
|
29
33
|
all_gaps = gap_repo.get_all()
|
|
30
34
|
blocking_gaps = [g for g in all_gaps if g.blocking]
|
|
31
|
-
|
|
35
|
+
|
|
32
36
|
if not blocking_gaps:
|
|
37
|
+
logger.info("dev repair: no blocking gaps; nothing to repair")
|
|
33
38
|
console.print("[green]No blocking gaps found. Nothing to repair![/green]")
|
|
34
39
|
return
|
|
35
40
|
|
|
41
|
+
logger.info("dev repair: %d blocking gap(s) across %d task(s)", len(blocking_gaps), len({g.task_id for g in blocking_gaps if g.task_id}))
|
|
36
42
|
console.print(f"Found [bold]{len(blocking_gaps)}[/bold] blocking gaps. Orchestrating repair plan...")
|
|
37
43
|
|
|
38
44
|
# Load router when credentials are available. Correction manifests have a
|
|
39
45
|
# deterministic fallback path, so missing model credentials must not block
|
|
40
46
|
# repair artifact generation.
|
|
41
47
|
repair_service = None
|
|
48
|
+
config = None
|
|
42
49
|
try:
|
|
43
50
|
config = load_config(root)
|
|
44
51
|
validate_model_provider(config.models.provider)
|
|
45
52
|
api_key = get_api_key(config.models.provider, root)
|
|
46
53
|
except (FileNotFoundError, ValueError) as e:
|
|
54
|
+
logger.warning("dev repair: no LLM provider (%s); using deterministic manifest fallback", e)
|
|
47
55
|
console.print(f"[yellow]{e}[/yellow]")
|
|
48
56
|
else:
|
|
49
|
-
provider = create_provider(config.models.provider, api_key, project_root=root)
|
|
57
|
+
provider = create_provider(config.models.provider, api_key, project_root=root, provider_prefs=config.provider)
|
|
50
58
|
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
51
59
|
router = ModelRouter(provider, role_config, project_root=root)
|
|
52
60
|
repair_service = RepairService(router)
|
|
@@ -60,7 +68,7 @@ async def run_repair_flow(project_root: Path = Path(".")):
|
|
|
60
68
|
task_ids = {gap.task_id for gap in blocking_gaps if gap.task_id}
|
|
61
69
|
for scoped_task_id in task_ids:
|
|
62
70
|
if scoped_task_id:
|
|
63
|
-
path = write_correction_manifest(root, scoped_task_id, repair_service=repair_service)
|
|
71
|
+
path = write_correction_manifest(root, scoped_task_id, repair_service=repair_service, config=config)
|
|
64
72
|
if path:
|
|
65
73
|
console.print(f" - Wrote correction manifest [dim]{path}[/dim]")
|
|
66
74
|
|
|
@@ -73,6 +81,7 @@ async def run_repair_flow(project_root: Path = Path(".")):
|
|
|
73
81
|
repair_count += 1
|
|
74
82
|
console.print(f" - Created intelligent repair task [bold]{task.id}[/bold]: {task.title}")
|
|
75
83
|
|
|
84
|
+
logger.info("dev repair complete: generated %d repair task(s)", repair_count)
|
|
76
85
|
console.print(f"\n[green]Successfully generated {repair_count} repair tasks.[/green]")
|
|
77
86
|
|
|
78
87
|
@app.callback(invoke_without_command=True)
|