devcouncil 0.1.0 → 0.2.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/LICENSE +201 -201
- package/README.md +197 -494
- package/package.json +9 -2
- package/pyproject.toml +62 -27
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +297 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +163 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- 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 +54 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +115 -54
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +31 -0
- package/src/devcouncil/cli/commands/doctor.py +291 -47
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +656 -0
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +209 -33
- package/src/devcouncil/cli/commands/init.py +204 -57
- package/src/devcouncil/cli/commands/integrate.py +1171 -76
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +96 -22
- package/src/devcouncil/cli/commands/plan.py +422 -210
- package/src/devcouncil/cli/commands/prompt.py +48 -34
- package/src/devcouncil/cli/commands/repair.py +89 -69
- package/src/devcouncil/cli/commands/report.py +120 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
- package/src/devcouncil/cli/commands/rollback.py +55 -54
- package/src/devcouncil/cli/commands/run.py +285 -220
- 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 +300 -20
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +141 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +49 -4
- package/src/devcouncil/cli/commands/verify.py +293 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +92 -25
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +47 -27
- package/src/devcouncil/domain/gap.py +52 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +55 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +186 -77
- package/src/devcouncil/execution/patch.py +77 -28
- package/src/devcouncil/execution/permissions.py +52 -59
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +650 -38
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +68 -64
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +736 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +186 -85
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +52 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
- package/src/devcouncil/gating/policy.py +315 -167
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +161 -0
- package/src/devcouncil/indexing/repo_mapper.py +1455 -204
- package/src/devcouncil/indexing/semantic_index.py +205 -0
- package/src/devcouncil/integrations/actions.py +146 -0
- package/src/devcouncil/integrations/check.py +423 -0
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +62 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +2072 -96
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +349 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +141 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +42 -38
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +627 -125
- package/src/devcouncil/llm/router.py +303 -118
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +71 -66
- package/src/devcouncil/planning/plan_service.py +60 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +70 -44
- package/src/devcouncil/repo/ci_scaffold.py +157 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +374 -0
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +30 -17
- package/src/devcouncil/reporting/markdown_report.py +83 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- 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 +330 -0
- package/src/devcouncil/storage/db.py +147 -66
- package/src/devcouncil/storage/models.py +204 -83
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +388 -249
- package/src/devcouncil/telemetry/cost.py +140 -34
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +52 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +423 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +147 -141
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/acceptance_compiler.py +125 -0
- package/src/devcouncil/verification/ad_hoc_check.py +129 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +178 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1342 -307
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import secrets
|
|
5
|
+
import time
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from devcouncil.app.project_status import compute_phase
|
|
12
|
+
from devcouncil.integrations.actions import apply_integration_target
|
|
13
|
+
from devcouncil.integrations.check import build_integration_check_report, integration_status_summary
|
|
14
|
+
from devcouncil.storage.db import get_db
|
|
15
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository, TaskRepository
|
|
16
|
+
from devcouncil.telemetry.traces import read_trace_events
|
|
17
|
+
|
|
18
|
+
LOGO_ASSET = "devcouncil_logo_premium.png"
|
|
19
|
+
LEGACY_LOGO_ASSET = "devcouncil-logo.svg"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# mtime-keyed cache so the dashboard's poll loop doesn't re-read and re-parse
|
|
23
|
+
# every run manifest on each refresh.
|
|
24
|
+
_RUN_MANIFEST_CACHE: dict[str, tuple[float, dict]] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_run_manifest(manifest_path: Path) -> dict | None:
|
|
28
|
+
key = str(manifest_path)
|
|
29
|
+
try:
|
|
30
|
+
mtime = manifest_path.stat().st_mtime
|
|
31
|
+
except OSError:
|
|
32
|
+
return None
|
|
33
|
+
cached = _RUN_MANIFEST_CACHE.get(key)
|
|
34
|
+
if cached is not None and cached[0] == mtime:
|
|
35
|
+
return dict(cached[1])
|
|
36
|
+
try:
|
|
37
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8")) or {}
|
|
38
|
+
except (json.JSONDecodeError, OSError):
|
|
39
|
+
return None
|
|
40
|
+
_RUN_MANIFEST_CACHE[key] = (mtime, manifest)
|
|
41
|
+
return dict(manifest)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def recent_run_artifacts(project_root: Path, *, limit: int = 10) -> list[dict]:
|
|
45
|
+
runs_dir = project_root / ".devcouncil" / "runs"
|
|
46
|
+
if not runs_dir.exists():
|
|
47
|
+
return []
|
|
48
|
+
manifests: list[dict] = []
|
|
49
|
+
for manifest_path in sorted(
|
|
50
|
+
runs_dir.glob("*/agent-run.json"),
|
|
51
|
+
key=lambda path: path.stat().st_mtime,
|
|
52
|
+
reverse=True,
|
|
53
|
+
):
|
|
54
|
+
manifest = _load_run_manifest(manifest_path)
|
|
55
|
+
if manifest is None:
|
|
56
|
+
continue
|
|
57
|
+
manifest["manifest_path"] = str(manifest_path)
|
|
58
|
+
manifests.append(manifest)
|
|
59
|
+
if len(manifests) >= limit:
|
|
60
|
+
break
|
|
61
|
+
return manifests
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Integration status probes the filesystem (and optionally CLIs) — far too
|
|
65
|
+
# expensive to recompute on every 2-second dashboard poll.
|
|
66
|
+
_INTEGRATION_SUMMARY_TTL_SECONDS = 5.0
|
|
67
|
+
_INTEGRATION_SUMMARY_CACHE: dict[str, tuple[float, dict]] = {}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _integration_summary_cached(project_root: Path) -> dict:
|
|
71
|
+
key = str(project_root)
|
|
72
|
+
now = time.monotonic()
|
|
73
|
+
cached = _INTEGRATION_SUMMARY_CACHE.get(key)
|
|
74
|
+
if cached is not None and now - cached[0] < _INTEGRATION_SUMMARY_TTL_SECONDS:
|
|
75
|
+
return cached[1]
|
|
76
|
+
summary = integration_status_summary(project_root)
|
|
77
|
+
_INTEGRATION_SUMMARY_CACHE[key] = (now, summary)
|
|
78
|
+
return summary
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _invalidate_integration_summary(project_root: Path) -> None:
|
|
82
|
+
_INTEGRATION_SUMMARY_CACHE.pop(str(project_root), None)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def logo_svg() -> str:
|
|
86
|
+
return resources.files("devcouncil.assets").joinpath(LEGACY_LOGO_ASSET).read_text(encoding="utf-8")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def logo_asset_bytes() -> bytes:
|
|
90
|
+
return resources.files("devcouncil.assets").joinpath(LOGO_ASSET).read_bytes()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def dashboard_payload(project_root: Path) -> dict:
|
|
94
|
+
db = get_db(project_root)
|
|
95
|
+
if not db:
|
|
96
|
+
return {
|
|
97
|
+
"initialized": False,
|
|
98
|
+
"phase": "UNINITIALIZED",
|
|
99
|
+
"tasks": [],
|
|
100
|
+
"coverage": {},
|
|
101
|
+
"events": [],
|
|
102
|
+
"integrations": _integration_summary_cached(project_root),
|
|
103
|
+
"recent_runs": recent_run_artifacts(project_root),
|
|
104
|
+
}
|
|
105
|
+
with db.get_session() as session:
|
|
106
|
+
graph = ArtifactGraphRepository(session).load_graph()
|
|
107
|
+
state = StateRepository(session).get_state()
|
|
108
|
+
phase = compute_phase(graph, state.current_phase if state else None)
|
|
109
|
+
tasks = [task.model_dump() for task in TaskRepository(session).get_all()]
|
|
110
|
+
return {
|
|
111
|
+
"initialized": True,
|
|
112
|
+
"phase": phase,
|
|
113
|
+
"coverage": graph.coverage_summary(),
|
|
114
|
+
"tasks": tasks,
|
|
115
|
+
"events": [event.model_dump(by_alias=True) for event in list(read_trace_events(project_root))[-50:]],
|
|
116
|
+
"integrations": _integration_summary_cached(project_root),
|
|
117
|
+
"recent_runs": recent_run_artifacts(project_root),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _json_response(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
|
|
122
|
+
body = json.dumps(payload).encode("utf-8")
|
|
123
|
+
handler.send_response(status)
|
|
124
|
+
handler.send_header("Content-Type", "application/json")
|
|
125
|
+
handler.send_header("Content-Length", str(len(body)))
|
|
126
|
+
handler.end_headers()
|
|
127
|
+
handler.wfile.write(body)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _is_loopback_client(handler: BaseHTTPRequestHandler) -> bool:
|
|
131
|
+
host = handler.client_address[0] if handler.client_address else ""
|
|
132
|
+
return host in {"127.0.0.1", "::1", "localhost"}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def dashboard_apply_payload(
|
|
136
|
+
project_root: Path,
|
|
137
|
+
raw_body: bytes,
|
|
138
|
+
*,
|
|
139
|
+
token: str,
|
|
140
|
+
provided_token: str | None,
|
|
141
|
+
) -> dict:
|
|
142
|
+
if not token or provided_token != token:
|
|
143
|
+
return {"ok": False, "error": "invalid dashboard token"}
|
|
144
|
+
try:
|
|
145
|
+
request = json.loads(raw_body.decode("utf-8") or "{}")
|
|
146
|
+
except json.JSONDecodeError:
|
|
147
|
+
return {"ok": False, "error": "invalid JSON body"}
|
|
148
|
+
if not isinstance(request, dict):
|
|
149
|
+
return {"ok": False, "error": "request body must be a JSON object"}
|
|
150
|
+
target = str(request.get("target") or "").strip()
|
|
151
|
+
include_hooks = bool(request.get("include_hooks", True))
|
|
152
|
+
strict = bool(request.get("strict", False))
|
|
153
|
+
try:
|
|
154
|
+
report = apply_integration_target(
|
|
155
|
+
project_root,
|
|
156
|
+
target,
|
|
157
|
+
include_hooks=include_hooks,
|
|
158
|
+
strict=strict,
|
|
159
|
+
)
|
|
160
|
+
except ValueError as exc:
|
|
161
|
+
return {"ok": False, "error": str(exc)}
|
|
162
|
+
# The next poll should reflect the freshly applied integration.
|
|
163
|
+
_invalidate_integration_summary(project_root)
|
|
164
|
+
return report.as_dict()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def dashboard_html(token: str = "") -> str:
|
|
168
|
+
safe_token = token.replace('"', "")
|
|
169
|
+
return f"""<!doctype html>
|
|
170
|
+
<html lang="en">
|
|
171
|
+
<head>
|
|
172
|
+
<meta charset="utf-8">
|
|
173
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
174
|
+
<meta name="devcouncil-dashboard-token" content="{safe_token}">
|
|
175
|
+
<title>DevCouncil Dashboard</title>
|
|
176
|
+
<style>
|
|
177
|
+
body {{ margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f7f7f5; color: #202124; }}
|
|
178
|
+
header {{ padding: 18px 28px; border-bottom: 1px solid #d9d9d4; background: #ffffff; display: flex; align-items: center; justify-content: space-between; gap: 18px; }}
|
|
179
|
+
main {{ padding: 24px 28px; display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }}
|
|
180
|
+
section {{ background: #ffffff; border: 1px solid #d9d9d4; border-radius: 8px; padding: 16px; }}
|
|
181
|
+
h1 {{ font-size: 20px; margin: 0; }}
|
|
182
|
+
h2 {{ font-size: 15px; margin: 0 0 12px; }}
|
|
183
|
+
.brand {{ display: flex; align-items: center; gap: 12px; min-width: 0; }}
|
|
184
|
+
.brand img {{ width: 54px; height: 54px; flex: 0 0 auto; filter: drop-shadow(0 8px 12px rgba(12, 36, 52, 0.22)); }}
|
|
185
|
+
.phase-line {{ white-space: nowrap; }}
|
|
186
|
+
.phase {{ font-weight: 700; }}
|
|
187
|
+
table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
|
|
188
|
+
th, td {{ text-align: left; border-bottom: 1px solid #ededeb; padding: 8px; vertical-align: top; }}
|
|
189
|
+
pre {{ margin: 0; white-space: pre-wrap; font-size: 12px; }}
|
|
190
|
+
.toolbar {{ display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }}
|
|
191
|
+
button {{ border: 1px solid #b9b9b2; background: #ffffff; color: #202124; border-radius: 6px; padding: 6px 10px; font: inherit; cursor: pointer; }}
|
|
192
|
+
button:disabled {{ color: #898984; cursor: default; background: #f3f3f0; }}
|
|
193
|
+
.status-pill {{ display: inline-block; min-width: 58px; padding: 2px 6px; border-radius: 999px; font-size: 12px; text-align: center; background: #ededeb; }}
|
|
194
|
+
.status-ok {{ background: #dff3e4; color: #155724; }}
|
|
195
|
+
.status-warn {{ background: #fff3cd; color: #664d03; }}
|
|
196
|
+
.status-fail {{ background: #f8d7da; color: #842029; }}
|
|
197
|
+
#integration-result {{ margin-top: 10px; font-size: 12px; }}
|
|
198
|
+
@media (max-width: 800px) {{ main {{ grid-template-columns: 1fr; padding: 16px; }} header {{ padding: 16px; }} }}
|
|
199
|
+
</style>
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
<header><div class="brand"><img src="/assets/devcouncil_logo_premium.png" alt="DevCouncil logo"><h1>DevCouncil Dashboard</h1></div><div class="phase-line">Phase: <span id="phase" class="phase">loading</span></div></header>
|
|
203
|
+
<main>
|
|
204
|
+
<section><h2>Coverage</h2><pre id="coverage">{{}}</pre></section>
|
|
205
|
+
<section><h2>Tasks</h2><table><thead><tr><th>ID</th><th>Status</th><th>Title</th></tr></thead><tbody id="tasks"></tbody></table></section>
|
|
206
|
+
<section style="grid-column: 1 / -1;">
|
|
207
|
+
<h2>CLI Integrations</h2>
|
|
208
|
+
<div class="toolbar">
|
|
209
|
+
<button type="button" data-target="all">Apply Detected</button>
|
|
210
|
+
<button type="button" data-target="hooks">Install Hooks</button>
|
|
211
|
+
<button type="button" id="run-check">Run Check</button>
|
|
212
|
+
</div>
|
|
213
|
+
<pre id="integration-result"></pre>
|
|
214
|
+
<table>
|
|
215
|
+
<thead><tr><th>Client</th><th>PATH</th><th>MCP</th><th>Hooks</th><th>Launcher</th><th>Configured</th><th>Notes</th><th>Action</th></tr></thead>
|
|
216
|
+
<tbody id="integrations"></tbody>
|
|
217
|
+
</table>
|
|
218
|
+
</section>
|
|
219
|
+
<section style="grid-column: 1 / -1;">
|
|
220
|
+
<h2>Recent Agent Runs</h2>
|
|
221
|
+
<table>
|
|
222
|
+
<thead><tr><th>Run</th><th>Task</th><th>Agent</th><th>Status</th><th>Transcript</th></tr></thead>
|
|
223
|
+
<tbody id="runs"></tbody>
|
|
224
|
+
</table>
|
|
225
|
+
</section>
|
|
226
|
+
<section style="grid-column: 1 / -1;"><h2>Recent Trace Events</h2><pre id="events"></pre></section>
|
|
227
|
+
</main>
|
|
228
|
+
<script>
|
|
229
|
+
function setText(cell, value) {{
|
|
230
|
+
cell.textContent = value == null ? '' : String(value);
|
|
231
|
+
return cell;
|
|
232
|
+
}}
|
|
233
|
+
|
|
234
|
+
function statusClass(value) {{
|
|
235
|
+
if (value === 'ok') return 'status-pill status-ok';
|
|
236
|
+
if (value === 'missing' || value === 'drifted') return 'status-pill status-warn';
|
|
237
|
+
return 'status-pill';
|
|
238
|
+
}}
|
|
239
|
+
|
|
240
|
+
function statusCell(value) {{
|
|
241
|
+
const cell = document.createElement('td');
|
|
242
|
+
const pill = document.createElement('span');
|
|
243
|
+
pill.className = statusClass(value);
|
|
244
|
+
pill.textContent = value || 'n/a';
|
|
245
|
+
cell.appendChild(pill);
|
|
246
|
+
return cell;
|
|
247
|
+
}}
|
|
248
|
+
|
|
249
|
+
const token = document.querySelector('meta[name="devcouncil-dashboard-token"]').content;
|
|
250
|
+
|
|
251
|
+
async function applyIntegration(target) {{
|
|
252
|
+
const resultBox = document.getElementById('integration-result');
|
|
253
|
+
resultBox.textContent = `Running ${{target}}...`;
|
|
254
|
+
const res = await fetch('/api/integrations/apply', {{
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {{
|
|
257
|
+
'Content-Type': 'application/json',
|
|
258
|
+
'X-DevCouncil-Dashboard-Token': token,
|
|
259
|
+
}},
|
|
260
|
+
body: JSON.stringify({{ target }}),
|
|
261
|
+
}});
|
|
262
|
+
const payload = await res.json();
|
|
263
|
+
resultBox.textContent = JSON.stringify(payload, null, 2);
|
|
264
|
+
await refresh();
|
|
265
|
+
}}
|
|
266
|
+
|
|
267
|
+
async function runIntegrationCheck() {{
|
|
268
|
+
const resultBox = document.getElementById('integration-result');
|
|
269
|
+
const res = await fetch('/api/integrations/check');
|
|
270
|
+
const payload = await res.json();
|
|
271
|
+
resultBox.textContent = JSON.stringify(payload, null, 2);
|
|
272
|
+
}}
|
|
273
|
+
|
|
274
|
+
async function refresh() {{
|
|
275
|
+
const res = await fetch('/api/status');
|
|
276
|
+
const data = await res.json();
|
|
277
|
+
document.getElementById('phase').textContent = data.phase;
|
|
278
|
+
document.getElementById('coverage').textContent = JSON.stringify(data.coverage, null, 2);
|
|
279
|
+
const body = document.getElementById('tasks');
|
|
280
|
+
body.replaceChildren(...(data.tasks || []).map(t => {{
|
|
281
|
+
const row = document.createElement('tr');
|
|
282
|
+
row.appendChild(setText(document.createElement('td'), t.id));
|
|
283
|
+
row.appendChild(setText(document.createElement('td'), t.status));
|
|
284
|
+
row.appendChild(setText(document.createElement('td'), t.title));
|
|
285
|
+
return row;
|
|
286
|
+
}}));
|
|
287
|
+
const integrationsBody = document.getElementById('integrations');
|
|
288
|
+
const capabilities = ((data.integrations || {{}}).capabilities || []);
|
|
289
|
+
integrationsBody.replaceChildren(...capabilities.map(item => {{
|
|
290
|
+
const row = document.createElement('tr');
|
|
291
|
+
row.appendChild(setText(document.createElement('td'), item.label || item.name));
|
|
292
|
+
row.appendChild(setText(document.createElement('td'), item.on_path ? 'yes' : 'no'));
|
|
293
|
+
row.appendChild(setText(document.createElement('td'), item.mcp ? 'yes' : 'no'));
|
|
294
|
+
row.appendChild(setText(document.createElement('td'), item.hooks ? 'yes' : 'verification'));
|
|
295
|
+
row.appendChild(setText(document.createElement('td'), item.launcher_shim ? 'yes' : 'no'));
|
|
296
|
+
row.appendChild(statusCell(item.config_status));
|
|
297
|
+
row.appendChild(setText(document.createElement('td'), item.notes || ''));
|
|
298
|
+
const action = document.createElement('td');
|
|
299
|
+
const button = document.createElement('button');
|
|
300
|
+
button.type = 'button';
|
|
301
|
+
button.dataset.target = item.apply_target || item.name;
|
|
302
|
+
button.textContent = item.config_status === 'ok' ? 'Reapply' : 'Fix';
|
|
303
|
+
button.disabled = !item.fixable;
|
|
304
|
+
action.appendChild(button);
|
|
305
|
+
row.appendChild(action);
|
|
306
|
+
return row;
|
|
307
|
+
}}));
|
|
308
|
+
|
|
309
|
+
const runsBody = document.getElementById('runs');
|
|
310
|
+
runsBody.replaceChildren(...(data.recent_runs || []).map(run => {{
|
|
311
|
+
const row = document.createElement('tr');
|
|
312
|
+
row.appendChild(setText(document.createElement('td'), run.run_id));
|
|
313
|
+
row.appendChild(setText(document.createElement('td'), run.task_id));
|
|
314
|
+
row.appendChild(setText(document.createElement('td'), run.agent));
|
|
315
|
+
row.appendChild(setText(document.createElement('td'), run.status || 'unknown'));
|
|
316
|
+
row.appendChild(setText(document.createElement('td'), run.transcript || ''));
|
|
317
|
+
return row;
|
|
318
|
+
}}));
|
|
319
|
+
|
|
320
|
+
document.getElementById('events').textContent = JSON.stringify(data.events || [], null, 2);
|
|
321
|
+
}}
|
|
322
|
+
|
|
323
|
+
document.addEventListener('click', event => {{
|
|
324
|
+
const button = event.target;
|
|
325
|
+
if (!(button instanceof HTMLButtonElement)) return;
|
|
326
|
+
if (button.id === 'run-check') {{
|
|
327
|
+
runIntegrationCheck();
|
|
328
|
+
return;
|
|
329
|
+
}}
|
|
330
|
+
const target = button.dataset.target;
|
|
331
|
+
if (target) applyIntegration(target);
|
|
332
|
+
}});
|
|
333
|
+
|
|
334
|
+
refresh();
|
|
335
|
+
setInterval(refresh, 2000);
|
|
336
|
+
</script>
|
|
337
|
+
</body>
|
|
338
|
+
</html>"""
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def run_dashboard(project_root: Path, host: str = "127.0.0.1", port: int = 8765) -> None:
|
|
342
|
+
dashboard_token = secrets.token_urlsafe(24)
|
|
343
|
+
|
|
344
|
+
class DashboardServer(ThreadingHTTPServer):
|
|
345
|
+
allow_reuse_address = True
|
|
346
|
+
daemon_threads = True
|
|
347
|
+
|
|
348
|
+
class Handler(BaseHTTPRequestHandler):
|
|
349
|
+
def do_GET(self): # noqa: N802
|
|
350
|
+
parsed = urlparse(self.path)
|
|
351
|
+
if parsed.path == f"/assets/{LOGO_ASSET}":
|
|
352
|
+
body = logo_asset_bytes()
|
|
353
|
+
self.send_response(200)
|
|
354
|
+
self.send_header("Content-Type", "image/png")
|
|
355
|
+
self.send_header("Cache-Control", "public, max-age=3600")
|
|
356
|
+
self.send_header("Content-Length", str(len(body)))
|
|
357
|
+
self.end_headers()
|
|
358
|
+
self.wfile.write(body)
|
|
359
|
+
return
|
|
360
|
+
if parsed.path == f"/assets/{LEGACY_LOGO_ASSET}":
|
|
361
|
+
body = logo_svg().encode("utf-8")
|
|
362
|
+
self.send_response(200)
|
|
363
|
+
self.send_header("Content-Type", "image/svg+xml; charset=utf-8")
|
|
364
|
+
self.send_header("Cache-Control", "public, max-age=3600")
|
|
365
|
+
self.send_header("Content-Length", str(len(body)))
|
|
366
|
+
self.end_headers()
|
|
367
|
+
self.wfile.write(body)
|
|
368
|
+
return
|
|
369
|
+
if parsed.path == "/api/integrations/check":
|
|
370
|
+
body = json.dumps(build_integration_check_report(project_root).as_dict()).encode("utf-8")
|
|
371
|
+
self.send_response(200)
|
|
372
|
+
self.send_header("Content-Type", "application/json")
|
|
373
|
+
self.send_header("Content-Length", str(len(body)))
|
|
374
|
+
self.end_headers()
|
|
375
|
+
self.wfile.write(body)
|
|
376
|
+
return
|
|
377
|
+
if parsed.path == "/api/status":
|
|
378
|
+
body = json.dumps(dashboard_payload(project_root)).encode("utf-8")
|
|
379
|
+
self.send_response(200)
|
|
380
|
+
self.send_header("Content-Type", "application/json")
|
|
381
|
+
self.send_header("Content-Length", str(len(body)))
|
|
382
|
+
self.end_headers()
|
|
383
|
+
self.wfile.write(body)
|
|
384
|
+
return
|
|
385
|
+
if parsed.path.startswith("/api/"):
|
|
386
|
+
body = json.dumps({"error": "Not found"}).encode("utf-8")
|
|
387
|
+
self.send_response(404)
|
|
388
|
+
self.send_header("Content-Type", "application/json")
|
|
389
|
+
self.send_header("Content-Length", str(len(body)))
|
|
390
|
+
self.end_headers()
|
|
391
|
+
self.wfile.write(body)
|
|
392
|
+
return
|
|
393
|
+
body = dashboard_html(dashboard_token).encode("utf-8")
|
|
394
|
+
self.send_response(200)
|
|
395
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
396
|
+
self.send_header("Content-Length", str(len(body)))
|
|
397
|
+
self.end_headers()
|
|
398
|
+
self.wfile.write(body)
|
|
399
|
+
|
|
400
|
+
def do_POST(self): # noqa: N802
|
|
401
|
+
parsed = urlparse(self.path)
|
|
402
|
+
if parsed.path != "/api/integrations/apply":
|
|
403
|
+
_json_response(self, 404, {"ok": False, "error": "Not found"})
|
|
404
|
+
return
|
|
405
|
+
if not _is_loopback_client(self):
|
|
406
|
+
_json_response(self, 403, {"ok": False, "error": "dashboard mutations require loopback client"})
|
|
407
|
+
return
|
|
408
|
+
length = int(self.headers.get("Content-Length") or "0")
|
|
409
|
+
raw_body = self.rfile.read(length)
|
|
410
|
+
provided = self.headers.get("X-DevCouncil-Dashboard-Token")
|
|
411
|
+
payload = dashboard_apply_payload(
|
|
412
|
+
project_root,
|
|
413
|
+
raw_body,
|
|
414
|
+
token=dashboard_token,
|
|
415
|
+
provided_token=provided,
|
|
416
|
+
)
|
|
417
|
+
_json_response(self, 200 if payload.get("ok") else 403, payload)
|
|
418
|
+
|
|
419
|
+
def log_message(self, format, *args): # noqa: A002
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
server = DashboardServer((host, port), Handler)
|
|
423
|
+
server.serve_forever()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"""Utilities package: redaction, paths, hashing, subprocess helpers."""
|
|
1
|
+
"""Utilities package: redaction, paths, hashing, subprocess helpers."""
|